From 30af5da33043192dff626e869f2628ffc709f836 Mon Sep 17 00:00:00 2001
From: Maxim Filippov <colixer@gmail.com>
Date: Thu, 14 Nov 2019 23:44:07 +0900
Subject: [PATCH 01/20] Admin API: list all statuses from a given instance

---
 CHANGELOG.md                                  |  1 +
 lib/pleroma/web/activity_pub/activity_pub.ex  | 26 +++++++++++++
 .../web/admin_api/admin_api_controller.ex     | 15 ++++++++
 lib/pleroma/web/router.ex                     |  2 +
 .../admin_api/admin_api_controller_test.exs   | 37 +++++++++++++++++++
 5 files changed, 81 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4ec084dbd..66644efc9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -60,6 +60,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Mastodon API: Add `/api/v1/markers` for managing timeline read markers
 - Mastodon API: Add the `recipients` parameter to `GET /api/v1/conversations`
 - Configuration: `feed` option for user atom feed.
+- Admin API: Add `/api/pleroma/admin/instances/:instance/statuses` - lists all statuses from a given instance
 </details>
 
 ### Fixed
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 65dd251f3..8b5bd83e2 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -708,6 +708,17 @@ def fetch_user_activities(user, reading_user, params \\ %{}) do
     |> Enum.reverse()
   end
 
+  def fetch_instance_activities(params) do
+    params =
+      params
+      |> Map.put("type", ["Create", "Announce"])
+      |> Map.put("instance", params["instance"])
+      |> Map.put("whole_db", true)
+
+    fetch_activities([Pleroma.Constants.as_public()], params, :offset)
+    |> Enum.reverse()
+  end
+
   defp user_activities_recipients(%{"godmode" => true}) do
     []
   end
@@ -935,6 +946,20 @@ defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user}) do
 
   defp restrict_muted_reblogs(query, _), do: query
 
+  defp restrict_instance(query, %{"instance" => instance}) do
+    users =
+      from(
+        u in User,
+        select: u.ap_id,
+        where: fragment("? LIKE ?", u.nickname, ^"%@#{instance}")
+      )
+      |> Repo.all()
+
+    from(activity in query, where: activity.actor in ^users)
+  end
+
+  defp restrict_instance(query, _), do: query
+
   defp exclude_poll_votes(query, %{"include_poll_votes" => true}), do: query
 
   defp exclude_poll_votes(query, _) do
@@ -1015,6 +1040,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
     |> restrict_reblogs(opts)
     |> restrict_pinned(opts)
     |> restrict_muted_reblogs(opts)
+    |> restrict_instance(opts)
     |> Activity.restrict_deactivated_users()
     |> exclude_poll_votes(opts)
     |> exclude_visibility(opts)
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 30fc01755..2f43f018b 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -226,6 +226,21 @@ def user_show(conn, %{"nickname" => nickname}) do
     end
   end
 
+  def list_instance_statuses(conn, %{"instance" => instance} = params) do
+    {page, page_size} = page_params(params)
+
+    activities =
+      ActivityPub.fetch_instance_activities(%{
+        "instance" => instance,
+        "limit" => page_size,
+        "offset" => (page - 1) * page_size
+      })
+
+    conn
+    |> put_view(StatusView)
+    |> render("index.json", %{activities: activities, as: :activity})
+  end
+
   def list_user_statuses(conn, %{"nickname" => nickname} = params) do
     godmode = params["godmode"] == "true" || params["godmode"] == true
 
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 8fb4aec13..1e2982b67 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -177,6 +177,8 @@ defmodule Pleroma.Web.Router do
     get("/users/:nickname", AdminAPIController, :user_show)
     get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
 
+    get("/instances/:instance/statuses", AdminAPIController, :list_instance_statuses)
+
     get("/reports", AdminAPIController, :list_reports)
     get("/reports/:id", AdminAPIController, :report_show)
     put("/reports/:id", AdminAPIController, :report_update_state)
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index 2a9e4f5a0..5958e6462 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -2640,6 +2640,43 @@ test "DELETE /relay", %{admin: admin} do
                "@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin"
     end
   end
+
+  describe "instances" do
+    test "GET /instances/:instance/statuses" do
+      admin = insert(:user, is_admin: true)
+      user = insert(:user, local: false, nickname: "archaeme@archae.me")
+      user2 = insert(:user, local: false, nickname: "test@test.com")
+      insert_pair(:note_activity, user: user)
+      insert(:note_activity, user: user2)
+
+      conn =
+        build_conn()
+        |> assign(:user, admin)
+        |> get("/api/pleroma/admin/instances/archae.me/statuses")
+
+      response = json_response(conn, 200)
+
+      assert length(response) == 2
+
+      conn =
+        build_conn()
+        |> assign(:user, admin)
+        |> get("/api/pleroma/admin/instances/test.com/statuses")
+
+      response = json_response(conn, 200)
+
+      assert length(response) == 1
+
+      conn =
+        build_conn()
+        |> assign(:user, admin)
+        |> get("/api/pleroma/admin/instances/nonexistent.com/statuses")
+
+      response = json_response(conn, 200)
+
+      assert length(response) == 0
+    end
+  end
 end
 
 # Needed for testing

From c506cc48ef230da30d5786285806de904b725981 Mon Sep 17 00:00:00 2001
From: Maxim Filippov <colixer@gmail.com>
Date: Sat, 16 Nov 2019 18:44:48 +0900
Subject: [PATCH 02/20] Admin API: Error when trying to update reports in the
 "old" format

---
 CHANGELOG.md                          | 1 +
 lib/pleroma/web/activity_pub/utils.ex | 8 +++++++-
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b4ad91b0d..740facf04 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -77,6 +77,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 - Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
 - Mastodon API: Inability to get some local users by nickname in `/api/v1/accounts/:id_or_nickname`
+- Admin API: Error when trying to update reports in the "old" format
 </details>
 
 ## [1.1.2] - 2019-10-18
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index c45662359..01aacbde3 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -903,7 +903,13 @@ def update_report_state(_, _), do: {:error, "Unsupported state"}
 
   def strip_report_status_data(activity) do
     [actor | reported_activities] = activity.data["object"]
-    stripped_activities = Enum.map(reported_activities, & &1["id"])
+
+    stripped_activities =
+      Enum.map(reported_activities, fn
+        act when is_map(act) -> act["id"]
+        act when is_binary(act) -> act
+      end)
+
     new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
 
     {:ok, %{activity | data: new_data}}

From 9f2993044098ced1585c8886c16da19dd046b5fd Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Sat, 23 Nov 2019 22:55:41 +0300
Subject: [PATCH 03/20] fetcher: move local object checking into a reusable
 function

---
 lib/pleroma/object.ex         | 4 ++++
 lib/pleroma/object/fetcher.ex | 2 +-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index cde0eddd9..b4ed3a9b2 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -255,4 +255,8 @@ def update_data(%Object{data: data} = object, attrs \\ %{}) do
     |> Object.change(%{data: Map.merge(data || %{}, attrs)})
     |> Repo.update()
   end
+
+  def local?(%Object{data: %{"id" => id}}) do
+    String.starts_with?(id, Pleroma.Web.base_url() <> "/")
+  end
 end
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
index 9a9a46550..4d71c91a8 100644
--- a/lib/pleroma/object/fetcher.ex
+++ b/lib/pleroma/object/fetcher.ex
@@ -49,7 +49,7 @@ defp reinject_object(struct, data) do
   end
 
   def refetch_object(%Object{data: %{"id" => id}} = object) do
-    with {:local, false} <- {:local, String.starts_with?(id, Pleroma.Web.base_url() <> "/")},
+    with {:local, false} <- {:local, Object.local?(object)},
          {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
          {:ok, object} <- reinject_object(object, data) do
       {:ok, object}

From d3656c2725fa80ea66dd9de2d74893fe9888ef57 Mon Sep 17 00:00:00 2001
From: Sadposter <hannah+pleroma@coffee-and-dreams.uk>
Date: Mon, 25 Nov 2019 09:53:11 +0000
Subject: [PATCH 04/20] add ability to set a custom user-agent string

---
 config/config.exs                  |  1 +
 docs/configuration/cheatsheet.md   |  7 +++++++
 lib/pleroma/application.ex         | 10 ++++++++--
 test/http/request_builder_test.exs | 10 ++++++++++
 4 files changed, 26 insertions(+), 2 deletions(-)

diff --git a/config/config.exs b/config/config.exs
index 1e36d3314..b60ffef7d 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -209,6 +209,7 @@
 config :pleroma, :http,
   proxy_url: nil,
   send_user_agent: true,
+  user_agent: :default,
   adapter: [
     ssl_options: [
       # Workaround for remote server certificate chain issues
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index 07d9a1d45..082906293 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -74,6 +74,13 @@ You shouldn't edit the base config directly to avoid breakages and merge conflic
 
 * `dynamic_configuration`: Allow transferring configuration to DB with the subsequent customization from Admin api.
 
+## :http
+
+* `proxy_url`: an upstream proxy to fetch posts and/or media with, (default: `nil`)
+* `send_user_agent`: should we include a user agent with HTTP requests? (default: `true`)
+* `user_agent`: what user agent should  we use? (default: `:default`), must be string or `:default`
+* `adapter`: array of hackney options
+
 ## Federation
 ### MRF policies
 
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 2b6a55f98..9dbd1e26b 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -17,8 +17,14 @@ def named_version, do: @name <> " " <> @version
   def repository, do: @repository
 
   def user_agent do
-    info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>"
-    named_version() <> "; " <> info
+    case Pleroma.Config.get([:http, :user_agent], :default) do
+      :default ->
+        info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>"
+        named_version() <> "; " <> info
+
+      custom ->
+        custom
+    end
   end
 
   # See http://elixir-lang.org/docs/stable/elixir/Application.html
diff --git a/test/http/request_builder_test.exs b/test/http/request_builder_test.exs
index 170ca916f..80ef25d7b 100644
--- a/test/http/request_builder_test.exs
+++ b/test/http/request_builder_test.exs
@@ -16,11 +16,21 @@ test "don't send pleroma user agent" do
 
     test "send pleroma user agent" do
       Pleroma.Config.put([:http, :send_user_agent], true)
+      Pleroma.Config.put([:http, :user_agent], :default)
 
       assert RequestBuilder.headers(%{}, []) == %{
                headers: [{"User-Agent", Pleroma.Application.user_agent()}]
              }
     end
+
+    test "send custom user agent" do
+      Pleroma.Config.put([:http, :send_user_agent], true)
+      Pleroma.Config.put([:http, :user_agent], "totally-not-pleroma")
+
+      assert RequestBuilder.headers(%{}, []) == %{
+               headers: [{"User-Agent", "totally-not-pleroma"}]
+             }
+    end
   end
 
   describe "add_optional_params/3" do

From e2dc89069d6d77a10848e53f321e2e464f539a25 Mon Sep 17 00:00:00 2001
From: Sadposter <hannah+pleroma@coffee-and-dreams.uk>
Date: Mon, 25 Nov 2019 11:26:42 +0000
Subject: [PATCH 05/20] move documentation section for http client

---
 docs/configuration/cheatsheet.md | 19 +++++++++++--------
 1 file changed, 11 insertions(+), 8 deletions(-)

diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index 082906293..dc2f55229 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -74,13 +74,6 @@ You shouldn't edit the base config directly to avoid breakages and merge conflic
 
 * `dynamic_configuration`: Allow transferring configuration to DB with the subsequent customization from Admin api.
 
-## :http
-
-* `proxy_url`: an upstream proxy to fetch posts and/or media with, (default: `nil`)
-* `send_user_agent`: should we include a user agent with HTTP requests? (default: `true`)
-* `user_agent`: what user agent should  we use? (default: `:default`), must be string or `:default`
-* `adapter`: array of hackney options
-
 ## Federation
 ### MRF policies
 
@@ -355,7 +348,17 @@ Available caches:
 * `:activity_pub` - activity pub routes (except question activities). Defaults to `nil` (no expiration).
 * `:activity_pub_question` - activity pub routes (question activities). Defaults to `30_000` (30 seconds).
 
-## :hackney_pools
+## HTTP client
+
+### :http
+
+* `proxy_url`: an upstream proxy to fetch posts and/or media with, (default: `nil`)
+* `send_user_agent`: should we include a user agent with HTTP requests? (default: `true`)
+* `user_agent`: what user agent should  we use? (default: `:default`), must be string or `:default`
+* `adapter`: array of hackney options
+
+
+### :hackney_pools
 
 Advanced. Tweaks Hackney (http client) connections pools.
 

From 02f7383891ff0a8dd17f00d6d00ec3495116e38a Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Mon, 25 Nov 2019 17:19:33 +0300
Subject: [PATCH 06/20] ActivityPub controller: do not render remote users

---
 .../web/activity_pub/activity_pub_controller.ex     |  3 ++-
 .../activity_pub/activity_pub_controller_test.exs   | 13 +++++++++++++
 2 files changed, 15 insertions(+), 1 deletion(-)

diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index b2cd965fe..dec5da0d3 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -45,7 +45,7 @@ def relay_active?(conn, _) do
   end
 
   def user(conn, %{"nickname" => nickname}) do
-    with %User{} = user <- User.get_cached_by_nickname(nickname),
+    with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
          {:ok, user} <- User.ensure_keys_present(user) do
       conn
       |> put_resp_content_type("application/activity+json")
@@ -53,6 +53,7 @@ def user(conn, %{"nickname" => nickname}) do
       |> render("user.json", %{user: user})
     else
       nil -> {:error, :not_found}
+      %{local: false} -> {:error, :not_found}
     end
   end
 
diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs
index a5414c521..1aa73d75c 100644
--- a/test/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/web/activity_pub/activity_pub_controller_test.exs
@@ -110,6 +110,19 @@ test "it returns a json representation of the user with accept application/ld+js
 
       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
   end
 
   describe "/object/:uuid" do

From 4b10804f21b80b74f9f9f85c9ecd6e1ec791255d Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Mon, 25 Nov 2019 17:55:17 +0300
Subject: [PATCH 07/20] OStatus controller: don't serve json at /notice/,
 redirect instead

---
 lib/pleroma/web/ostatus/ostatus_controller.ex | 46 ++++-------
 test/web/ostatus/ostatus_controller_test.exs  | 79 ++++---------------
 2 files changed, 28 insertions(+), 97 deletions(-)

diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex
index 12a7c2365..01ec7941e 100644
--- a/lib/pleroma/web/ostatus/ostatus_controller.ex
+++ b/lib/pleroma/web/ostatus/ostatus_controller.ex
@@ -11,7 +11,6 @@ defmodule Pleroma.Web.OStatus.OStatusController do
   alias Pleroma.Plugs.RateLimiter
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPubController
-  alias Pleroma.Web.ActivityPub.ObjectView
   alias Pleroma.Web.ActivityPub.Visibility
   alias Pleroma.Web.Endpoint
   alias Pleroma.Web.Metadata.PlayerView
@@ -38,11 +37,9 @@ def object(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do
     with id <- o_status_url(conn, :object, uuid),
          {_, %Activity{} = activity} <-
            {:activity, Activity.get_create_by_object_ap_id_with_object(id)},
-         {_, true} <- {:public?, Visibility.is_public?(activity)},
-         %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
+         {_, true} <- {:public?, Visibility.is_public?(activity)} do
       case format do
-        "html" -> redirect(conn, to: "/notice/#{activity.id}")
-        _ -> represent_activity(conn, nil, activity, user)
+        _ -> redirect(conn, to: "/notice/#{activity.id}")
       end
     else
       reason when reason in [{:public?, false}, {:activity, nil}] ->
@@ -61,11 +58,9 @@ def activity(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
   def activity(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do
     with id <- o_status_url(conn, :activity, uuid),
          {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
-         {_, true} <- {:public?, Visibility.is_public?(activity)},
-         %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
+         {_, true} <- {:public?, Visibility.is_public?(activity)} do
       case format do
-        "html" -> redirect(conn, to: "/notice/#{activity.id}")
-        _ -> represent_activity(conn, format, activity, user)
+        _ -> redirect(conn, to: "/notice/#{activity.id}")
       end
     else
       reason when reason in [{:public?, false}, {:activity, nil}] ->
@@ -81,7 +76,15 @@ def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do
          {_, true} <- {:public?, Visibility.is_public?(activity)},
          %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
       cond do
-        format == "html" && activity.data["type"] == "Create" ->
+        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(
@@ -94,11 +97,8 @@ def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do
             }
           )
 
-        format == "html" ->
-          RedirectController.redirector(conn, nil)
-
         true ->
-          represent_activity(conn, format, activity, user)
+          RedirectController.redirector(conn, nil)
       end
     else
       reason when reason in [{:public?, false}, {:activity, nil}] ->
@@ -135,24 +135,6 @@ def notice_player(conn, %{"id" => id}) do
     end
   end
 
-  defp represent_activity(
-         conn,
-         "activity+json",
-         %Activity{data: %{"type" => "Create"}} = activity,
-         _user
-       ) do
-    object = Object.normalize(activity)
-
-    conn
-    |> put_resp_header("content-type", "application/activity+json")
-    |> put_view(ObjectView)
-    |> render("object.json", %{object: object})
-  end
-
-  defp represent_activity(_conn, _, _, _) do
-    {:error, :not_found}
-  end
-
   def errors(conn, {:error, :not_found}) do
     render_error(conn, :not_found, "Not found")
   end
diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs
index 37b7b62f5..50235dfef 100644
--- a/test/web/ostatus/ostatus_controller_test.exs
+++ b/test/web/ostatus/ostatus_controller_test.exs
@@ -35,23 +35,6 @@ test "redirects to /notice/id for html format", %{conn: conn} do
       assert redirected_to(conn) == "/notice/#{note_activity.id}"
     end
 
-    test "500s when user not found", %{conn: conn} do
-      note_activity = insert(:note_activity)
-      object = Object.normalize(note_activity)
-      user = User.get_cached_by_ap_id(note_activity.data["actor"])
-      User.invalidate_cache(user)
-      Pleroma.Repo.delete(user)
-      [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"]))
-      url = "/objects/#{uuid}"
-
-      conn =
-        conn
-        |> put_req_header("accept", "application/xml")
-        |> get(url)
-
-      assert response(conn, 500) == ~S({"error":"Something went wrong"})
-    end
-
     test "404s on private objects", %{conn: conn} do
       note_activity = insert(:direct_note_activity)
       object = Object.normalize(note_activity)
@@ -82,21 +65,6 @@ test "redirects to /notice/id for html format", %{conn: conn} do
       assert redirected_to(conn) == "/notice/#{note_activity.id}"
     end
 
-    test "505s when user not found", %{conn: conn} do
-      note_activity = insert(:note_activity)
-      [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
-      user = User.get_cached_by_ap_id(note_activity.data["actor"])
-      User.invalidate_cache(user)
-      Pleroma.Repo.delete(user)
-
-      conn =
-        conn
-        |> put_req_header("accept", "text/html")
-        |> get("/activities/#{uuid}")
-
-      assert response(conn, 500) == ~S({"error":"Something went wrong"})
-    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"]))
@@ -127,21 +95,28 @@ test "gets an activity in AS2 format", %{conn: conn} do
   end
 
   describe "GET notice/2" do
-    test "gets a notice in xml format", %{conn: conn} 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"]
 
-      conn
-      |> get("/notice/#{note_activity.id}")
-      |> response(200)
+      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 "gets a notice in AS2 format", %{conn: conn} do
-      note_activity = insert(:note_activity)
+    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}")
-      |> json_response(200)
+      |> response(404)
     end
 
     test "500s when actor not found", %{conn: conn} do
@@ -157,32 +132,6 @@ test "500s when actor not found", %{conn: conn} do
       assert response(conn, 500) == ~S({"error":"Something went wrong"})
     end
 
-    test "only gets a notice in AS2 format for Create messages", %{conn: conn} do
-      note_activity = insert(:note_activity)
-      url = "/notice/#{note_activity.id}"
-
-      conn =
-        conn
-        |> put_req_header("accept", "application/activity+json")
-        |> get(url)
-
-      assert json_response(conn, 200)
-
-      user = insert(:user)
-
-      {:ok, like_activity, _} = CommonAPI.favorite(note_activity.id, user)
-      url = "/notice/#{like_activity.id}"
-
-      assert like_activity.data["type"] == "Like"
-
-      conn =
-        build_conn()
-        |> put_req_header("accept", "application/activity+json")
-        |> get(url)
-
-      assert response(conn, 404)
-    end
-
     test "render html for redirect for html format", %{conn: conn} do
       note_activity = insert(:note_activity)
 

From f673e3deef8b8094a7a5fa012756c468901e5cac Mon Sep 17 00:00:00 2001
From: Mark Felder <feld@FreeBSD.org>
Date: Tue, 26 Nov 2019 05:46:48 -0600
Subject: [PATCH 08/20] Default log level for syslog in prod should also be
 :warn

---
 config/prod.exs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/config/prod.exs b/config/prod.exs
index 301d2b9cb..25873f360 100644
--- a/config/prod.exs
+++ b/config/prod.exs
@@ -21,6 +21,7 @@
 
 # Do not print debug messages in production
 config :logger, :console, level: :warn
+config :logger, :ex_syslogger, level: :warn
 
 # ## SSL Support
 #

From 80ededc04f4d4ba8318290976025b645a0ba3f95 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Tue, 26 Nov 2019 19:53:43 +0700
Subject: [PATCH 09/20] Add `direct_conversation_id` to web push payload

---
 lib/pleroma/activity.ex                           | 13 +++++++++++++
 lib/pleroma/web/mastodon_api/views/status_view.ex | 10 ++--------
 lib/pleroma/web/push/impl.ex                      |  6 +++++-
 3 files changed, 20 insertions(+), 9 deletions(-)

diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index 7e283df32..cd7a5aae9 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -303,4 +303,17 @@ def restrict_deactivated_users(query) do
   end
 
   defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
+
+  def direct_conversation_id(activity, for_user) do
+    alias Pleroma.Conversation.Participation
+
+    with %{data: %{"context" => context}} when is_binary(context) <- activity,
+         %Pleroma.Conversation{} = conversation <- Pleroma.Conversation.get_for_ap_id(context),
+         %Participation{id: participation_id} <-
+           Participation.for_user_and_conversation(for_user, conversation) do
+      participation_id
+    else
+      _ -> 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 baff54151..a0257dfa6 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -9,8 +9,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
 
   alias Pleroma.Activity
   alias Pleroma.ActivityExpiration
-  alias Pleroma.Conversation
-  alias Pleroma.Conversation.Participation
   alias Pleroma.HTML
   alias Pleroma.Object
   alias Pleroma.Repo
@@ -245,12 +243,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
     direct_conversation_id =
       with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]},
            {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
-           {_, %User{} = for_user} <- {:for_user, opts[:for]},
-           %{data: %{"context" => context}} when is_binary(context) <- activity,
-           %Conversation{} = conversation <- Conversation.get_for_ap_id(context),
-           %Participation{id: participation_id} <-
-             Participation.for_user_and_conversation(for_user, conversation) do
-        participation_id
+           {_, %User{} = for_user} <- {:for_user, opts[:for]} do
+        Activity.direct_conversation_id(activity, for_user)
       else
         {:direct_conversation_id, participation_id} when is_integer(participation_id) ->
           participation_id
diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex
index dd445e8bf..b01ae0f80 100644
--- a/lib/pleroma/web/push/impl.ex
+++ b/lib/pleroma/web/push/impl.ex
@@ -33,6 +33,8 @@ def perform(
     gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
     avatar_url = User.avatar_url(actor)
     object = Object.normalize(activity)
+    user = User.get_cached_by_id(user_id)
+    direct_conversation_id = Activity.direct_conversation_id(activity, user)
 
     for subscription <- fetch_subsriptions(user_id),
         get_in(subscription.data, ["alerts", type]) do
@@ -45,7 +47,9 @@ def perform(
         icon: avatar_url,
         preferred_locale: "en",
         pleroma: %{
-          activity_id: activity_id
+          activity_id: activity_id,
+          direct_conversation_id: direct_conversation_id,
+          account: user.ap_id
         }
       }
       |> Jason.encode!()

From 4af69f047db815a2349cadc757d5cff39174d83d Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Wed, 27 Nov 2019 02:32:55 +0700
Subject: [PATCH 10/20] Remove `account` field from web push payload

---
 lib/pleroma/web/push/impl.ex | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex
index b01ae0f80..3de7af708 100644
--- a/lib/pleroma/web/push/impl.ex
+++ b/lib/pleroma/web/push/impl.ex
@@ -48,8 +48,7 @@ def perform(
         preferred_locale: "en",
         pleroma: %{
           activity_id: activity_id,
-          direct_conversation_id: direct_conversation_id,
-          account: user.ap_id
+          direct_conversation_id: direct_conversation_id
         }
       }
       |> Jason.encode!()

From f01881e82a48b4177f943c73338d013010719040 Mon Sep 17 00:00:00 2001
From: Chris McCord <chris@chrismccord.com>
Date: Thu, 11 Jul 2019 16:33:20 -0400
Subject: [PATCH 11/20] Add federated test infrastructure

---
 test/federation/federation_test.exs |  43 ++++++
 test/support/cluster.ex             | 218 ++++++++++++++++++++++++++++
 2 files changed, 261 insertions(+)
 create mode 100644 test/federation/federation_test.exs
 create mode 100644 test/support/cluster.ex

diff --git a/test/federation/federation_test.exs b/test/federation/federation_test.exs
new file mode 100644
index 000000000..7400d2abc
--- /dev/null
+++ b/test/federation/federation_test.exs
@@ -0,0 +1,43 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Integration.FederationTest do
+  use Pleroma.DataCase
+
+  import Pleroma.Cluster
+
+  @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 = mastodon_api_url(Pleroma.Web.Endpoint, :user, user)
+          url_404 = mastodon_api_url(Pleroma.Web.Endpoint, :user, "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/support/cluster.ex b/test/support/cluster.ex
new file mode 100644
index 000000000..deb37f361
--- /dev/null
+++ b/test/support/cluster.ex
@@ -0,0 +1,218 @@
+defmodule Pleroma.Cluster do
+  @moduledoc """
+  Facilities for managing a cluster of slave VM's for federated testing.
+
+  ## Spawning the federated cluster
+
+  `spawn_cluster/1` spawns a map of slave nodes that are started
+  within the running VM. During startup, the slave node is sent all configuration
+  from the parent node, as well as all code. After receiving configuration and
+  code, the slave then starts all applications currently running on the parent.
+  The configuration passed to `spawn_cluster/1` overrides any parent application
+  configuration for the provided OTP app and key. This is useful for customizing
+  the Ecto database, Phoenix webserver ports, etc.
+
+  For example, to start a single federated VM named ":federated1", with the
+  Pleroma Endpoint running on port 4123, and with a database named
+  "pleroma_test1", you would run:
+
+    endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint)
+    repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo)
+
+    Pleroma.Cluster.spawn_cluster(%{
+      :"federated1@127.0.0.1" => [
+        {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test1")},
+        {:pleroma, Pleroma.Web.Endpoint,
+        Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)}
+      ]
+    })
+
+  *Note*: application configuration for a given key is not merged,
+  so any customization requires first fetching the existing values
+  and merging yourself by providing the merged configuration,
+  such as above with the endpoint config and repo config.
+
+  ## Executing code within a remote node
+
+  Use the `within/2` macro to execute code within the context of a remote
+  federated node. The code block captures all local variable bindings from
+  the parent's context and returns the result of the expression after executing
+  it on the remote node. For example:
+
+      import Pleroma.Cluster
+
+      parent_value = 123
+
+      result =
+        within :"federated1@127.0.0.1" do
+          {node(), parent_value}
+        end
+
+      assert result == {:"federated1@127.0.0.1, 123}
+
+  *Note*: while local bindings are captured and available within the block,
+  other parent contexts like required, aliased, or imported modules are not
+  in scope. Those will need to be reimported/aliases/required within the block
+  as `within/2` is a remote procedure call.
+  """
+
+  @extra_apps Pleroma.Mixfile.application()[:extra_applications]
+
+  @doc """
+  Spawns the default Pleroma federated cluster.
+
+  Values before may be customized as needed for the test suite.
+  """
+  def spawn_default_cluster do
+    endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint)
+    repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo)
+
+    spawn_cluster(%{
+      :"federated1@127.0.0.1" => [
+        {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated1")},
+        {:pleroma, Pleroma.Web.Endpoint,
+         Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)}
+      ],
+      :"federated2@127.0.0.1" => [
+        {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated2")},
+        {:pleroma, Pleroma.Web.Endpoint,
+         Keyword.merge(endpoint_conf, http: [port: 4012], url: [port: 4012], server: true)}
+      ]
+    })
+  end
+
+  @doc """
+  Spawns a configured map of federated nodes.
+
+  See `Pleroma.Cluster` module documentation for details.
+  """
+  def spawn_cluster(node_configs) do
+    # Turn node into a distributed node with the given long name
+    :net_kernel.start([:"primary@127.0.0.1"])
+
+    # Allow spawned nodes to fetch all code from this node
+    {:ok, _} = :erl_boot_server.start([])
+    allow_boot("127.0.0.1")
+
+    silence_logger_warnings(fn ->
+      node_configs
+      |> Enum.map(&Task.async(fn -> start_slave(&1) end))
+      |> Enum.map(&Task.await(&1, 60_000))
+    end)
+  end
+
+  @doc """
+  Executes block of code again remote node.
+
+  See `Pleroma.Cluster` module documentation for details.
+  """
+  defmacro within(node, do: block) do
+    quote do
+      rpc(unquote(node), unquote(__MODULE__), :eval_quoted, [
+        unquote(Macro.escape(block)),
+        binding()
+      ])
+    end
+  end
+
+  @doc false
+  def eval_quoted(block, binding) do
+    {result, _binding} = Code.eval_quoted(block, binding, __ENV__)
+    result
+  end
+
+  defp start_slave({node_host, override_configs}) do
+    log(node_host, "booting federated VM")
+    {:ok, node} = :slave.start(~c"127.0.0.1", node_name(node_host), vm_args())
+    add_code_paths(node)
+    load_apps_and_transfer_configuration(node, override_configs)
+    ensure_apps_started(node)
+    {:ok, node}
+  end
+
+  def rpc(node, module, function, args) do
+    :rpc.block_call(node, module, function, args)
+  end
+
+  defp vm_args do
+    ~c"-loader inet -hosts 127.0.0.1 -setcookie #{:erlang.get_cookie()}"
+  end
+
+  defp allow_boot(host) do
+    {:ok, ipv4} = :inet.parse_ipv4_address(~c"#{host}")
+    :ok = :erl_boot_server.add_slave(ipv4)
+  end
+
+  defp add_code_paths(node) do
+    rpc(node, :code, :add_paths, [:code.get_path()])
+  end
+
+  defp load_apps_and_transfer_configuration(node, override_configs) do
+    Enum.each(Application.loaded_applications(), fn {app_name, _, _} ->
+      app_name
+      |> Application.get_all_env()
+      |> Enum.each(fn {key, primary_config} ->
+        rpc(node, Application, :put_env, [app_name, key, primary_config, [persistent: true]])
+      end)
+    end)
+
+    Enum.each(override_configs, fn {app_name, key, val} ->
+      rpc(node, Application, :put_env, [app_name, key, val, [persistent: true]])
+    end)
+  end
+
+  defp log(node, msg), do: IO.puts("[#{node}] #{msg}")
+
+  defp ensure_apps_started(node) do
+    loaded_names = Enum.map(Application.loaded_applications(), fn {name, _, _} -> name end)
+    app_names = @extra_apps ++ (loaded_names -- @extra_apps)
+
+    rpc(node, Application, :ensure_all_started, [:mix])
+    rpc(node, Mix, :env, [Mix.env()])
+    rpc(node, __MODULE__, :prepare_database, [])
+
+    log(node, "starting application")
+
+    Enum.reduce(app_names, MapSet.new(), fn app, loaded ->
+      if Enum.member?(loaded, app) do
+        loaded
+      else
+        {:ok, started} = rpc(node, Application, :ensure_all_started, [app])
+        MapSet.union(loaded, MapSet.new(started))
+      end
+    end)
+  end
+
+  @doc false
+  def prepare_database do
+    log(node(), "preparing database")
+    repo_config = Application.get_env(:pleroma, Pleroma.Repo)
+    repo_config[:adapter].storage_down(repo_config)
+    repo_config[:adapter].storage_up(repo_config)
+
+    {:ok, _, _} =
+      Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
+        Ecto.Migrator.run(repo, :up, log: false, all: true)
+      end)
+
+    Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)
+    {:ok, _} = Application.ensure_all_started(:ex_machina)
+  end
+
+  defp silence_logger_warnings(func) do
+    prev_level = Logger.level()
+    Logger.configure(level: :error)
+    res = func.()
+    Logger.configure(level: prev_level)
+
+    res
+  end
+
+  defp node_name(node_host) do
+    node_host
+    |> to_string()
+    |> String.split("@")
+    |> Enum.at(0)
+    |> String.to_atom()
+  end
+end

From 717d246491f65024957546fbbbaa7209d92d96d1 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Mon, 22 Jul 2019 16:17:18 +0200
Subject: [PATCH 12/20] Tests: Don't run federated tests by default

---
 test/federation/federation_test.exs | 8 ++++++--
 test/test_helper.exs                | 3 +--
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/test/federation/federation_test.exs b/test/federation/federation_test.exs
index 7400d2abc..8b8e8c870 100644
--- a/test/federation/federation_test.exs
+++ b/test/federation/federation_test.exs
@@ -4,11 +4,15 @@
 
 defmodule Pleroma.Integration.FederationTest do
   use Pleroma.DataCase
-
+  @moduletag :federated
   import Pleroma.Cluster
 
-  @federated1 :"federated1@127.0.0.1"
+  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
diff --git a/test/test_helper.exs b/test/test_helper.exs
index c8dbee010..b80c2683e 100644
--- a/test/test_helper.exs
+++ b/test/test_helper.exs
@@ -2,8 +2,7 @@
 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
-os_exclude = if :os.type() == {:unix, :darwin}, do: [skip_on_mac: true], else: []
-ExUnit.start(exclude: os_exclude)
+ExUnit.start(exclude: [:federated])
 Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)
 Mox.defmock(Pleroma.ReverseProxy.ClientMock, for: Pleroma.ReverseProxy.Client)
 {:ok, _} = Application.ensure_all_started(:ex_machina)

From 74aa42998f23f49df1ea43564645ecbea3007c19 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Mon, 22 Jul 2019 16:18:37 +0200
Subject: [PATCH 13/20] CI: Add testing stage for federated tests.

---
 .gitlab-ci.yml | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ab62c8827..78b560cac 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -55,6 +55,18 @@ unit-testing:
     - mix ecto.migrate
     - mix coveralls --preload-modules
 
+federated-testing:
+  stage: test
+  services:
+  - name: lainsoykaf/postgres-with-rum
+    alias: postgres
+    command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
+  script:
+    - mix deps.get
+    - mix ecto.create
+    - mix ecto.migrate
+    - mix test --trace --only federated
+
 unit-testing-rum:
   stage: test
   services:

From bbec9bb727baf6c76656b977fd8fbdd63aaaae07 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Mon, 22 Jul 2019 16:32:50 +0200
Subject: [PATCH 14/20] Tests:  Run the daemon

---
 .gitlab-ci.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 78b560cac..66fa806f8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -65,6 +65,7 @@ federated-testing:
     - mix deps.get
     - mix ecto.create
     - mix ecto.migrate
+    - epmd -daemon
     - mix test --trace --only federated
 
 unit-testing-rum:

From bd1984f054b075c04f2098afc5fe326fbc610c10 Mon Sep 17 00:00:00 2001
From: stwf <steven.fuchs@dockyard.com>
Date: Tue, 26 Nov 2019 15:24:34 -0500
Subject: [PATCH 15/20] update tests and Oban

---
 mix.exs                             | 2 +-
 mix.lock                            | 8 ++++----
 test/federation/federation_test.exs | 4 ++--
 test/test_helper.exs                | 4 +++-
 4 files changed, 10 insertions(+), 8 deletions(-)

diff --git a/mix.exs b/mix.exs
index 60c7fe3f6..eb2e54e4a 100644
--- a/mix.exs
+++ b/mix.exs
@@ -102,7 +102,7 @@ defp deps do
       {:phoenix_ecto, "~> 4.0"},
       {:ecto_sql, "~> 3.2"},
       {:postgrex, ">= 0.13.5"},
-      {:oban, "~> 0.8.1"},
+      {:oban, "~> 0.12.0"},
       {:quantum, "~> 2.3"},
       {:gettext, "~> 0.15"},
       {:comeonin, "~> 4.1.1"},
diff --git a/mix.lock b/mix.lock
index 3a664287c..0d41c4ad5 100644
--- a/mix.lock
+++ b/mix.lock
@@ -23,8 +23,8 @@
   "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"},
   "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm"},
   "earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm"},
-  "ecto": {:hex, :ecto, "3.2.3", "51274df79862845b388733fddcf6f107d0c8c86e27abe7131fa98f8d30761bda", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
-  "ecto_sql": {:hex, :ecto_sql, "3.2.0", "751cea597e8deb616084894dd75cbabfdbe7255ff01e8c058ca13f0353a3921b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.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"},
+  "ecto": {:hex, :ecto, "3.2.5", "76c864b77948a479e18e69cc1d0f0f4ee7cced1148ffe6a093ff91eba644f0b5", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
+  "ecto_sql": {:hex, :ecto_sql, "3.2.2", "d10845bc147b9f61ef485cbf0973c0a337237199bd9bd30dd9542db00aadc26b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0 or ~> 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"},
   "esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [:mix], [], "hexpm"},
   "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm"},
   "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"},
@@ -67,7 +67,7 @@
   "myhtmlex": {:git, "https://git.pleroma.social/pleroma/myhtmlex.git", "ad0097e2f61d4953bfef20fb6abddf23b87111e6", [ref: "ad0097e2f61d4953bfef20fb6abddf23b87111e6", submodules: true]},
   "nimble_parsec": {:hex, :nimble_parsec, "0.5.1", "c90796ecee0289dbb5ad16d3ad06f957b0cd1199769641c961cfe0b97db190e0", [:mix], [], "hexpm"},
   "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
-  "oban": {:hex, :oban, "0.8.1", "4bbf62eb1829f856d69aeb5069ac7036afe07db8221a17de2a9169cc7a58a318", [: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"},
+  "oban": {:hex, :oban, "0.12.0", "5477d5ab4a5a201c0b6c89764040ebfc5d2c71c488a36f378016ce5990838f0f", [: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"},
   "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
   "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm"},
   "phoenix": {:hex, :phoenix, "1.4.10", "619e4a545505f562cd294df52294372d012823f4fd9d34a6657a8b242898c255", [: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"},
@@ -97,7 +97,7 @@
   "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm"},
   "swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [: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: false]}, {: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"},
   "syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]},
-  "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"},
+  "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm"},
   "tesla": {:hex, :tesla, "1.3.0", "f35d72f029e608f9cdc6f6d6fcc7c66cf6d6512a70cfef9206b21b8bd0203a30", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 0.4", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"},
   "timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [: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"},
   "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
diff --git a/test/federation/federation_test.exs b/test/federation/federation_test.exs
index 8b8e8c870..45800568a 100644
--- a/test/federation/federation_test.exs
+++ b/test/federation/federation_test.exs
@@ -33,8 +33,8 @@ test "runs webserver on customized port" do
         within @federated1 do
           import Pleroma.Web.Router.Helpers
           user = Pleroma.Factory.insert(:user)
-          user_url = mastodon_api_url(Pleroma.Web.Endpoint, :user, user)
-          url_404 = mastodon_api_url(Pleroma.Web.Endpoint, :user, "not-exists")
+          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
diff --git a/test/test_helper.exs b/test/test_helper.exs
index b80c2683e..241ad1f94 100644
--- a/test/test_helper.exs
+++ b/test/test_helper.exs
@@ -2,7 +2,9 @@
 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
-ExUnit.start(exclude: [:federated])
+os_exclude = if :os.type() == {:unix, :darwin}, do: [skip_on_mac: true], else: []
+ExUnit.start(exclude: [:federated | os_exclude])
+
 Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)
 Mox.defmock(Pleroma.ReverseProxy.ClientMock, for: Pleroma.ReverseProxy.Client)
 {:ok, _} = Application.ensure_all_started(:ex_machina)

From f595cfe6230715681e8ba93f73480bf695b3a243 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Wed, 27 Nov 2019 19:43:47 +0700
Subject: [PATCH 16/20] Remove User.user_info/2

---
 lib/pleroma/user.ex                           | 25 ------
 .../web/mastodon_api/views/account_view.ex    | 11 ++-
 test/user_test.exs                            | 78 ++++---------------
 .../mastodon_api/views/account_view_test.exs  |  3 +-
 4 files changed, 23 insertions(+), 94 deletions(-)

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 3010fe87f..b18a4c6a5 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -177,20 +177,6 @@ def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
   def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
   def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
 
-  def user_info(%User{} = user, args \\ %{}) do
-    following_count = Map.get(args, :following_count, user.following_count)
-    follower_count = Map.get(args, :follower_count, user.follower_count)
-
-    %{
-      note_count: user.note_count,
-      locked: user.locked,
-      confirmation_pending: user.confirmation_pending,
-      default_scope: user.default_scope,
-      follower_count: follower_count,
-      following_count: following_count
-    }
-  end
-
   def follow_state(%User{} = user, %User{} = target) do
     case Utils.fetch_latest_follow(user, target) do
       %{data: %{"state" => state}} -> state
@@ -209,10 +195,6 @@ def set_follow_state_cache(user_ap_id, target_ap_id, state) do
     Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
   end
 
-  def set_info_cache(user, args) do
-    Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
-  end
-
   @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
   def restrict_deactivated(query) do
     from(u in query, where: u.deactivated != ^true)
@@ -614,7 +596,6 @@ def set_cache({:error, err}), do: {:error, err}
   def set_cache(%User{} = user) do
     Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
     Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
-    Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
     {:ok, user}
   end
 
@@ -633,7 +614,6 @@ def update_and_set_cache(changeset) do
   def invalidate_cache(user) do
     Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
     Cachex.del(:user_cache, "nickname:#{user.nickname}")
-    Cachex.del(:user_cache, "user_info:#{user.id}")
   end
 
   def get_cached_by_ap_id(ap_id) do
@@ -701,11 +681,6 @@ def get_by_nickname_or_email(nickname_or_email) do
     get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
   end
 
-  def get_cached_user_info(user) do
-    key = "user_info:#{user.id}"
-    Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
-  end
-
   def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
 
   def get_or_fetch_by_nickname(nickname) do
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index e30fed610..7f50f3aa6 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -71,18 +71,17 @@ defp do_render("show.json", %{user: user} = opts) do
 
     image = User.avatar_url(user) |> MediaProxy.url()
     header = User.banner_url(user) |> MediaProxy.url()
-    user_info = User.get_cached_user_info(user)
 
     following_count =
       if !user.hide_follows_count or !user.hide_follows or opts[:for] == user do
-        user_info.following_count
+        user.following_count
       else
         0
       end
 
     followers_count =
       if !user.hide_followers_count or !user.hide_followers or opts[:for] == user do
-        user_info.follower_count
+        user.follower_count
       else
         0
       end
@@ -144,7 +143,7 @@ defp do_render("show.json", %{user: user} = opts) do
 
       # Pleroma extension
       pleroma: %{
-        confirmation_pending: user_info.confirmation_pending,
+        confirmation_pending: user.confirmation_pending,
         tags: user.tags,
         hide_followers_count: user.hide_followers_count,
         hide_follows_count: user.hide_follows_count,
@@ -157,7 +156,7 @@ defp do_render("show.json", %{user: user} = opts) do
       }
     }
     |> maybe_put_role(user, opts[:for])
-    |> maybe_put_settings(user, opts[:for], user_info)
+    |> maybe_put_settings(user, opts[:for], opts)
     |> maybe_put_notification_settings(user, opts[:for])
     |> maybe_put_settings_store(user, opts[:for], opts)
     |> maybe_put_chat_token(user, opts[:for], opts)
@@ -191,7 +190,7 @@ defp maybe_put_settings(
          data,
          %User{id: user_id} = user,
          %User{id: user_id},
-         _user_info
+         _opts
        ) do
     data
     |> Kernel.put_in([:source, :privacy], user.default_scope)
diff --git a/test/user_test.exs b/test/user_test.exs
index e6302b525..82e338e75 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -961,9 +961,9 @@ test "hide a user from followers" do
       {:ok, user} = User.follow(user, user2)
       {:ok, _user} = User.deactivate(user)
 
-      info = User.get_cached_user_info(user2)
+      user2 = User.get_cached_by_id(user2.id)
 
-      assert info.follower_count == 0
+      assert user2.follower_count == 0
       assert [] = User.get_followers(user2)
     end
 
@@ -977,10 +977,10 @@ test "hide a user from friends" do
 
       {:ok, _user} = User.deactivate(user)
 
-      info = User.get_cached_user_info(user2)
+      user2 = User.get_cached_by_id(user2.id)
 
       assert refresh_record(user2).following_count == 0
-      assert info.following_count == 0
+      assert user2.following_count == 0
       assert User.following_count(user2) == 0
       assert [] = User.get_friends(user2)
     end
@@ -1182,13 +1182,12 @@ test "html_filter_policy returns TwitterText scrubber when rich-text is disabled
   describe "caching" do
     test "invalidate_cache works" do
       user = insert(:user)
-      _user_info = User.get_cached_user_info(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}")
-      {:ok, nil} = Cachex.get(:user_cache, "user_info:#{user.id}")
     end
 
     test "User.delete() plugs any possible zombie objects" do
@@ -1344,7 +1343,7 @@ test "follower count is updated when a follower is blocked" do
 
     {:ok, user} = User.block(user, follower)
 
-    assert User.user_info(user).follower_count == 2
+    assert user.follower_count == 2
   end
 
   describe "list_inactive_users_query/1" do
@@ -1521,51 +1520,6 @@ test "external_users/1 external active users with limit", %{user1: user1, user2:
     end
   end
 
-  describe "set_info_cache/2" do
-    setup do
-      user = insert(:user)
-      {:ok, user: user}
-    end
-
-    test "update from args", %{user: user} do
-      User.set_info_cache(user, %{following_count: 15, follower_count: 18})
-
-      %{follower_count: followers, following_count: following} = User.get_cached_user_info(user)
-      assert followers == 18
-      assert following == 15
-    end
-
-    test "without args", %{user: user} do
-      User.set_info_cache(user, %{})
-
-      %{follower_count: followers, following_count: following} = User.get_cached_user_info(user)
-      assert followers == 0
-      assert following == 0
-    end
-  end
-
-  describe "user_info/2" do
-    setup do
-      user = insert(:user)
-      {:ok, user: user}
-    end
-
-    test "update from args", %{user: user} do
-      %{follower_count: followers, following_count: following} =
-        User.user_info(user, %{following_count: 15, follower_count: 18})
-
-      assert followers == 18
-      assert following == 15
-    end
-
-    test "without args", %{user: user} do
-      %{follower_count: followers, following_count: following} = User.user_info(user)
-
-      assert followers == 0
-      assert following == 0
-    end
-  end
-
   describe "is_internal_user?/1" do
     test "non-internal user returns false" do
       user = insert(:user)
@@ -1622,14 +1576,14 @@ test "updates the counters normally on following/getting a follow when disabled"
           ap_enabled: true
         )
 
-      assert User.user_info(other_user).following_count == 0
-      assert User.user_info(other_user).follower_count == 0
+      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.user_info(user).following_count == 1
-      assert User.user_info(other_user).follower_count == 1
+      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
@@ -1645,14 +1599,14 @@ test "syncronizes the counters with the remote instance for the followed when en
           ap_enabled: true
         )
 
-      assert User.user_info(other_user).following_count == 0
-      assert User.user_info(other_user).follower_count == 0
+      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 User.user_info(other_user).follower_count == 437
+      assert other_user.follower_count == 437
     end
 
     test "syncronizes the counters with the remote instance for the follower when enabled" do
@@ -1668,13 +1622,13 @@ test "syncronizes the counters with the remote instance for the follower when en
           ap_enabled: true
         )
 
-      assert User.user_info(other_user).following_count == 0
-      assert User.user_info(other_user).follower_count == 0
+      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 User.user_info(other_user).following_count == 152
+      assert other_user.following_count == 152
     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 af88841ed..15bfcbb78 100644
--- a/test/web/mastodon_api/views/account_view_test.exs
+++ b/test/web/mastodon_api/views/account_view_test.exs
@@ -350,7 +350,8 @@ test "represent an embedded relationship" do
       }
     }
 
-    assert expected == AccountView.render("show.json", %{user: user, for: other_user})
+    assert expected ==
+             AccountView.render("show.json", %{user: refresh_record(user), for: other_user})
   end
 
   test "returns the settings store if the requesting user is the represented user and it's requested specifically" do

From f36724efb1fc0806e11b4c0c32a03d1aeee13da6 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Wed, 27 Nov 2019 14:13:36 +0100
Subject: [PATCH 17/20] User: Never return nil for user follower counts.

---
 lib/pleroma/user.ex | 4 ++--
 test/user_test.exs  | 8 ++++++++
 2 files changed, 10 insertions(+), 2 deletions(-)

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 3010fe87f..16d0889c4 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -178,8 +178,8 @@ def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
   def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
 
   def user_info(%User{} = user, args \\ %{}) do
-    following_count = Map.get(args, :following_count, user.following_count)
-    follower_count = Map.get(args, :follower_count, user.follower_count)
+    following_count = Map.get(args, :following_count, user.following_count) || 0
+    follower_count = Map.get(args, :follower_count, user.follower_count) || 0
 
     %{
       note_count: user.note_count,
diff --git a/test/user_test.exs b/test/user_test.exs
index e6302b525..4917ea9ce 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -1347,6 +1347,14 @@ test "follower count is updated when a follower is blocked" do
     assert User.user_info(user).follower_count == 2
   end
 
+  test "with nil follower count fields, 0 will be returned" do
+    user = insert(:user, follower_count: nil, following_count: nil)
+    user_info = User.user_info(user)
+
+    assert user_info.follower_count == 0
+    assert user_info.following_count == 0
+  end
+
   describe "list_inactive_users_query/1" do
     defp days_ago(days) do
       NaiveDateTime.add(

From c4b468a95bd22dae16d89024016b2590e38d23ae Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Thu, 28 Nov 2019 16:44:48 +0700
Subject: [PATCH 18/20] Cleanup tests output

---
 test/web/activity_pub/transmogrifier_test.exs    | 5 +++++
 test/web/admin_api/admin_api_controller_test.exs | 1 +
 test/web/streamer/streamer_test.exs              | 2 +-
 3 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index 0bdd514e9..b31c411dc 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -39,6 +39,7 @@ test "it ignores an incoming notice if we already have it" do
       assert activity == returned_activity
     end
 
+    @tag capture_log: true
     test "it fetches replied-to activities if we don't have them" do
       data =
         File.read!("test/fixtures/mastodon-post-activity.json")
@@ -533,6 +534,7 @@ test "it works for incoming announces with an inlined activity" do
       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
       data =
         File.read!("test/fixtures/bogus-mastodon-announce.json")
@@ -814,6 +816,7 @@ test "it fails for incoming deletes with spoofed origin" do
       assert Activity.get_by_id(activity.id)
     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")
 
@@ -1749,6 +1752,7 @@ test "returns object with inReplyToAtomUri when denied incoming reply", %{data:
       assert modified_object["inReplyToAtomUri"] == ""
     end
 
+    @tag capture_log: true
     test "returns modified object when allowed incoming reply", %{data: data} do
       object_with_reply =
         Map.put(
@@ -1868,6 +1872,7 @@ test "returns nil when cannot normalize object" do
              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://shitposter.club/notice/2827873")
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index bb2ca6a62..32577afee 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -1923,6 +1923,7 @@ test "with settings in db", %{conn: conn} do
       Pleroma.Config.put([:instance, :dynamic_configuration], true)
     end
 
+    @tag capture_log: true
     test "create new config setting in db", %{conn: conn} do
       conn =
         post(conn, "/api/pleroma/admin/config", %{
diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs
index 80a7541b2..8265f18dd 100644
--- a/test/web/streamer/streamer_test.exs
+++ b/test/web/streamer/streamer_test.exs
@@ -15,7 +15,7 @@ defmodule Pleroma.Web.StreamerTest do
   alias Pleroma.Web.Streamer.StreamerSocket
   alias Pleroma.Web.Streamer.Worker
 
-  @moduletag needs_streamer: true
+  @moduletag needs_streamer: true, capture_log: true
   clear_config_all([:instance, :skip_thread_containment])
 
   describe "user streams" do

From 9a2d38107ccaf8730a51ac052c73153d385a5cb6 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Thu, 28 Nov 2019 15:14:11 +0100
Subject: [PATCH 19/20] CI: Make benchmarks manually triggered.

---
 .gitlab-ci.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ab62c8827..c4cde5cb9 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -31,6 +31,7 @@ build:
 
 benchmark:
   stage: benchmark
+  when: manual
   variables:
     MIX_ENV: benchmark
   services:

From 798aa0c3350ba24b962c54bd1b9e5a14a70a76c0 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Thu, 28 Nov 2019 23:08:13 +0700
Subject: [PATCH 20/20] Use PosgreSQL v12 in the federated tests

---
 .gitlab-ci.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 66fa806f8..1a5df7f7e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -58,7 +58,7 @@ unit-testing:
 federated-testing:
   stage: test
   services:
-  - name: lainsoykaf/postgres-with-rum
+  - name: minibikini/postgres-with-rum:12
     alias: postgres
     command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
   script: