Move subscription notifications to a separate controller
authorRoman Chvanikov <chvanikoff@pm.me>
Mon, 16 Sep 2019 18:59:49 +0000 (21:59 +0300)
committerRoman Chvanikov <chvanikoff@pm.me>
Mon, 16 Sep 2019 18:59:49 +0000 (21:59 +0300)
lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
lib/pleroma/web/mastodon_api/mastodon_api.ex
lib/pleroma/web/pleroma_api/pleroma_api.ex [new file with mode: 0644]
lib/pleroma/web/pleroma_api/subscription_notification_controller.ex [new file with mode: 0644]
lib/pleroma/web/pleroma_api/views/subscription_notification_view.ex [moved from lib/pleroma/web/mastodon_api/views/subscription_notification_view.ex with 93% similarity]
lib/pleroma/web/router.ex
test/web/mastodon_api/mastodon_api_controller_test.exs
test/web/pleroma_api/subscription_notification_controller_test.exs [new file with mode: 0644]

index eefdb8c06ecb28e914c6065daa8c016f32601e4a..060137b80e38fe45945c6d8d34892e3830f86c0b 100644 (file)
@@ -23,7 +23,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   alias Pleroma.Repo
   alias Pleroma.ScheduledActivity
   alias Pleroma.Stats
-  alias Pleroma.SubscriptionNotification
   alias Pleroma.User
   alias Pleroma.Web
   alias Pleroma.Web.ActivityPub.ActivityPub
@@ -40,7 +39,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   alias Pleroma.Web.MastodonAPI.ReportView
   alias Pleroma.Web.MastodonAPI.ScheduledActivityView
   alias Pleroma.Web.MastodonAPI.StatusView
-  alias Pleroma.Web.MastodonAPI.SubscriptionNotificationView
   alias Pleroma.Web.MediaProxy
   alias Pleroma.Web.OAuth.App
   alias Pleroma.Web.OAuth.Authorization
@@ -727,28 +725,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     |> render("index.json", %{notifications: notifications, for: user})
   end
 
-  def subscription_notifications(%{assigns: %{user: user}} = conn, params) do
-    notifications = MastodonAPI.get_subscription_notifications(user, params)
-
-    conn
-    |> add_link_headers(:subscription_notifications, notifications)
-    |> put_view(SubscriptionNotificationView)
-    |> render("index.json", %{notifications: notifications, for: user})
-  end
-
-  def get_subscription_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
-    with {:ok, notification} <- SubscriptionNotification.get(user, id) do
-      conn
-      |> put_view(SubscriptionNotificationView)
-      |> render("show.json", %{subscription_notification: notification, for: user})
-    else
-      {:error, reason} ->
-        conn
-        |> put_status(:forbidden)
-        |> json(%{"error" => reason})
-    end
-  end
-
   def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
     with {:ok, notification} <- Notification.get(user, id) do
       conn
@@ -767,11 +743,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     json(conn, %{})
   end
 
-  def clear_subscription_notifications(%{assigns: %{user: user}} = conn, _params) do
-    SubscriptionNotification.clear(user)
-    json(conn, %{})
-  end
-
   def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
     with {:ok, _notif} <- Notification.dismiss(user, id) do
       json(conn, %{})
@@ -783,30 +754,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     end
   end
 
-  def dismiss_subscription_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
-    with {:ok, _notif} <- SubscriptionNotification.dismiss(user, id) do
-      json(conn, %{})
-    else
-      {:error, reason} ->
-        conn
-        |> put_status(:forbidden)
-        |> json(%{"error" => reason})
-    end
-  end
-
   def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
     Notification.destroy_multiple(user, ids)
     json(conn, %{})
   end
 
-  def destroy_multiple_subscription_notifications(
-        %{assigns: %{user: user}} = conn,
-        %{"ids" => ids} = _params
-      ) do
-    SubscriptionNotification.destroy_multiple(user, ids)
-    json(conn, %{})
-  end
-
   def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
     id = List.wrap(id)
     q = from(u in User, where: u.id in ^id)
index 6751e24d849ebd99c969d2e3dc8e86d4bc662b28..ac01d1ff39a42639f4b457b780b5893e0429c3e5 100644 (file)
@@ -10,7 +10,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
   alias Pleroma.Notification
   alias Pleroma.Pagination
   alias Pleroma.ScheduledActivity
-  alias Pleroma.SubscriptionNotification
   alias Pleroma.User
   alias Pleroma.Web.CommonAPI
 
@@ -63,15 +62,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
     |> Pagination.fetch_paginated(params)
   end
 
-  def get_subscription_notifications(user, params \\ %{}) do
-    options = cast_params(params)
-
-    user
-    |> SubscriptionNotification.for_user_query(options)
-    |> restrict(:exclude_types, options)
-    |> Pagination.fetch_paginated(params)
-  end
-
   def get_scheduled_activities(user, params \\ %{}) do
     user
     |> ScheduledActivity.for_user_query()
diff --git a/lib/pleroma/web/pleroma_api/pleroma_api.ex b/lib/pleroma/web/pleroma_api/pleroma_api.ex
new file mode 100644 (file)
index 0000000..4809648
--- /dev/null
@@ -0,0 +1,40 @@
+defmodule Pleroma.Web.PleromaAPI.PleromaAPI do
+  import Ecto.Query
+  import Ecto.Changeset
+
+  alias Pleroma.Activity
+  alias Pleroma.Pagination
+  alias Pleroma.SubscriptionNotification
+
+  def get_subscription_notifications(user, params \\ %{}) do
+    options = cast_params(params)
+
+    user
+    |> SubscriptionNotification.for_user_query(options)
+    |> restrict(:exclude_types, options)
+    |> Pagination.fetch_paginated(params)
+  end
+
+  defp cast_params(params) do
+    param_types = %{
+      exclude_types: {:array, :string},
+      reblogs: :boolean,
+      with_muted: :boolean
+    }
+
+    changeset = cast({%{}, param_types}, params, Map.keys(param_types))
+    changeset.changes
+  end
+
+  defp restrict(query, :exclude_types, %{exclude_types: mastodon_types = [_ | _]}) do
+    ap_types =
+      mastodon_types
+      |> Enum.map(&Activity.from_mastodon_notification_type/1)
+      |> Enum.filter(& &1)
+
+    query
+    |> where([q, a], not fragment("? @> ARRAY[?->>'type']::varchar[]", ^ap_types, a.data))
+  end
+
+  defp restrict(query, _, _), do: query
+end
diff --git a/lib/pleroma/web/pleroma_api/subscription_notification_controller.ex b/lib/pleroma/web/pleroma_api/subscription_notification_controller.ex
new file mode 100644 (file)
index 0000000..bfc2631
--- /dev/null
@@ -0,0 +1,59 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.SubscriptionNotificationController do
+  use Pleroma.Web, :controller
+
+  import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
+
+  alias Pleroma.SubscriptionNotification
+  alias Pleroma.Web.PleromaAPI.PleromaAPI
+  alias Pleroma.Web.PleromaAPI.SubscriptionNotificationView
+
+  def list(%{assigns: %{user: user}} = conn, params) do
+    notifications = PleromaAPI.get_subscription_notifications(user, params)
+
+    conn
+    |> add_link_headers(notifications)
+    |> put_view(SubscriptionNotificationView)
+    |> render("index.json", %{notifications: notifications, for: user})
+  end
+
+  def get(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
+    with {:ok, notification} <- SubscriptionNotification.get(user, id) do
+      conn
+      |> put_view(SubscriptionNotificationView)
+      |> render("show.json", %{subscription_notification: notification, for: user})
+    else
+      {:error, reason} ->
+        conn
+        |> put_status(:forbidden)
+        |> json(%{"error" => reason})
+    end
+  end
+
+  def clear(%{assigns: %{user: user}} = conn, _params) do
+    SubscriptionNotification.clear(user)
+    json(conn, %{})
+  end
+
+  def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
+    with {:ok, _notif} <- SubscriptionNotification.dismiss(user, id) do
+      json(conn, %{})
+    else
+      {:error, reason} ->
+        conn
+        |> put_status(:forbidden)
+        |> json(%{"error" => reason})
+    end
+  end
+
+  def destroy_multiple(
+        %{assigns: %{user: user}} = conn,
+        %{"ids" => ids} = _params
+      ) do
+    SubscriptionNotification.destroy_multiple(user, ids)
+    json(conn, %{})
+  end
+end
similarity index 93%
rename from lib/pleroma/web/mastodon_api/views/subscription_notification_view.ex
rename to lib/pleroma/web/pleroma_api/views/subscription_notification_view.ex
index 83d2b647f7ce876b0819b3df8de553a3935703d0..d7f7f4c5abc802aed3307519d369220d9be203a7 100644 (file)
@@ -2,15 +2,15 @@
 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
-defmodule Pleroma.Web.MastodonAPI.SubscriptionNotificationView do
+defmodule Pleroma.Web.PleromaAPI.SubscriptionNotificationView do
   use Pleroma.Web, :view
 
   alias Pleroma.Activity
   alias Pleroma.User
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.MastodonAPI.AccountView
-  alias Pleroma.Web.MastodonAPI.SubscriptionNotificationView
   alias Pleroma.Web.MastodonAPI.StatusView
+  alias Pleroma.Web.PleromaAPI.SubscriptionNotificationView
 
   def render("index.json", %{notifications: notifications, for: user}) do
     safe_render_many(notifications, SubscriptionNotificationView, "show.json", %{for: user})
index 409fc9ecaa5886a182f6e084fdb1a731019b6fbd..05891b6c090adcd4ef3feece5ed8a845f1cc9d08 100644 (file)
@@ -268,6 +268,14 @@ defmodule Pleroma.Web.Router do
       pipe_through(:oauth_read)
       get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
       get("/conversations/:id", PleromaAPIController, :conversation)
+
+      scope "/subscription_notifications" do
+        post("/clear", SubscriptionNotificationController, :clear)
+        post("/dismiss", SubscriptionNotificationController, :dismiss)
+        delete("/destroy_multiple", SubscriptionNotificationController, :destroy_multiple)
+        get("/", SubscriptionNotificationController, :list)
+        get("/id", SubscriptionNotificationController, :get)
+      end
     end
 
     scope [] do
@@ -302,38 +310,13 @@ defmodule Pleroma.Web.Router do
 
       post("/notifications/clear", MastodonAPIController, :clear_notifications)
 
-      post(
-        "/notifications/subscription/clear",
-        MastodonAPIController,
-        :clear_subscription_notifications
-      )
-
       post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
 
-      post(
-        "/notifications/subscription/dismiss",
-        MastodonAPIController,
-        :dismiss_subscription_notification
-      )
-
       get("/notifications", MastodonAPIController, :notifications)
-      get("/notifications/subscription", MastodonAPIController, :subscription_notifications)
       get("/notifications/:id", MastodonAPIController, :get_notification)
 
-      get(
-        "/notifications/subscription/:id",
-        MastodonAPIController,
-        :get_subscription_notification
-      )
-
       delete("/notifications/destroy_multiple", MastodonAPIController, :destroy_multiple)
 
-      delete(
-        "/notifications/subscription/destroy_multiple",
-        MastodonAPIController,
-        :destroy_multiple_subscription_notifications
-      )
-
       get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses)
       get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status)
 
index 1d2d9e1349caa52b42d733c88e0a6f8046f1d4a1..fb04748bb447d04e4f9b6790a53ad4d28a760949 100644 (file)
@@ -13,7 +13,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
   alias Pleroma.Object
   alias Pleroma.Repo
   alias Pleroma.ScheduledActivity
-  alias Pleroma.SubscriptionNotification
   alias Pleroma.Tests.ObanHelpers
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPub
@@ -1275,197 +1274,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     end
   end
 
-  describe "subscription_notifications" do
-    setup do
-      user = insert(:user)
-      subscriber = insert(:user)
-
-      User.subscribe(subscriber, user)
-
-      {:ok, %{user: user, subscriber: subscriber}}
-    end
-
-    test "list of notifications", %{conn: conn, user: user, subscriber: subscriber} do
-      status_text = "Hello"
-      {:ok, _activity} = CommonAPI.post(user, %{"status" => status_text})
-
-      conn =
-        conn
-        |> assign(:user, subscriber)
-        |> get("/api/v1/notifications/subscription")
-
-      assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200)
-      assert response == status_text
-    end
-
-    test "getting a single notification", %{conn: conn, user: user, subscriber: subscriber} do
-      status_text = "Hello"
-
-      {:ok, _activity} = CommonAPI.post(user, %{"status" => status_text})
-      [notification] = Repo.all(SubscriptionNotification)
-
-      conn =
-        conn
-        |> assign(:user, subscriber)
-        |> get("/api/v1/notifications/subscription/#{notification.id}")
-
-      assert %{"status" => %{"content" => response}} = json_response(conn, 200)
-      assert response == status_text
-    end
-
-    test "dismissing a single notification also deletes it", %{
-      conn: conn,
-      user: user,
-      subscriber: subscriber
-    } do
-      status_text = "Hello"
-      {:ok, _activity} = CommonAPI.post(user, %{"status" => status_text})
-
-      [notification] = Repo.all(SubscriptionNotification)
-
-      conn =
-        conn
-        |> assign(:user, subscriber)
-        |> post("/api/v1/notifications/subscription/dismiss", %{"id" => notification.id})
-
-      assert %{} = json_response(conn, 200)
-
-      assert Repo.all(SubscriptionNotification) == []
-    end
-
-    test "clearing all notifications also deletes them", %{
-      conn: conn,
-      user: user,
-      subscriber: subscriber
-    } do
-      status_text1 = "Hello"
-      status_text2 = "Hello again"
-      {:ok, _activity1} = CommonAPI.post(user, %{"status" => status_text1})
-      {:ok, _activity2} = CommonAPI.post(user, %{"status" => status_text2})
-
-      conn =
-        conn
-        |> assign(:user, subscriber)
-        |> post("/api/v1/notifications/subscription/clear")
-
-      assert %{} = json_response(conn, 200)
-
-      conn =
-        build_conn()
-        |> assign(:user, subscriber)
-        |> get("/api/v1/notifications/subscription")
-
-      assert json_response(conn, 200) == []
-
-      assert Repo.all(SubscriptionNotification) == []
-    end
-
-    test "paginates notifications using min_id, since_id, max_id, and limit", %{
-      conn: conn,
-      user: user,
-      subscriber: subscriber
-    } do
-      {:ok, activity1} = CommonAPI.post(user, %{"status" => "Hello 1"})
-      {:ok, activity2} = CommonAPI.post(user, %{"status" => "Hello 2"})
-      {:ok, activity3} = CommonAPI.post(user, %{"status" => "Hello 3"})
-      {:ok, activity4} = CommonAPI.post(user, %{"status" => "Hello 4"})
-
-      notification1_id =
-        Repo.get_by(SubscriptionNotification, activity_id: activity1.id).id |> to_string()
-
-      notification2_id =
-        Repo.get_by(SubscriptionNotification, activity_id: activity2.id).id |> to_string()
-
-      notification3_id =
-        Repo.get_by(SubscriptionNotification, activity_id: activity3.id).id |> to_string()
-
-      notification4_id =
-        Repo.get_by(SubscriptionNotification, activity_id: activity4.id).id |> to_string()
-
-      conn = assign(conn, :user, subscriber)
-
-      # min_id
-      conn_res =
-        get(conn, "/api/v1/notifications/subscription?limit=2&min_id=#{notification1_id}")
-
-      result = json_response(conn_res, 200)
-      assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
-
-      # since_id
-      conn_res =
-        get(conn, "/api/v1/notifications/subscription?limit=2&since_id=#{notification1_id}")
-
-      result = json_response(conn_res, 200)
-      assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
-
-      # max_id
-      conn_res =
-        get(conn, "/api/v1/notifications/subscription?limit=2&max_id=#{notification4_id}")
-
-      result = json_response(conn_res, 200)
-      assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
-    end
-
-    test "destroy multiple", %{conn: conn, user: user1, subscriber: user2} do
-      # mutual subscription
-      User.subscribe(user1, user2)
-
-      {:ok, activity1} = CommonAPI.post(user1, %{"status" => "Hello 1"})
-      {:ok, activity2} = CommonAPI.post(user1, %{"status" => "World 1"})
-      {:ok, activity3} = CommonAPI.post(user2, %{"status" => "Hello 2"})
-      {:ok, activity4} = CommonAPI.post(user2, %{"status" => "World 2"})
-
-      notification1_id =
-        Repo.get_by(SubscriptionNotification, activity_id: activity1.id).id |> to_string()
-
-      notification2_id =
-        Repo.get_by(SubscriptionNotification, activity_id: activity2.id).id |> to_string()
-
-      notification3_id =
-        Repo.get_by(SubscriptionNotification, activity_id: activity3.id).id |> to_string()
-
-      notification4_id =
-        Repo.get_by(SubscriptionNotification, activity_id: activity4.id).id |> to_string()
-
-      conn = assign(conn, :user, user1)
-
-      conn_res = get(conn, "/api/v1/notifications/subscription")
-
-      result = json_response(conn_res, 200)
-
-      Enum.each(result, fn %{"id" => id} ->
-        assert id in [notification3_id, notification4_id]
-      end)
-
-      conn2 = assign(conn, :user, user2)
-
-      conn_res = get(conn2, "/api/v1/notifications/subscription")
-
-      result = json_response(conn_res, 200)
-
-      Enum.each(result, fn %{"id" => id} ->
-        assert id in [notification1_id, notification2_id]
-      end)
-
-      conn_destroy =
-        delete(conn, "/api/v1/notifications/subscription/destroy_multiple", %{
-          "ids" => [notification3_id, notification4_id]
-        })
-
-      assert json_response(conn_destroy, 200) == %{}
-
-      conn_res = get(conn2, "/api/v1/notifications/subscription")
-
-      result = json_response(conn_res, 200)
-
-      Enum.each(result, fn %{"id" => id} ->
-        assert id in [notification1_id, notification2_id]
-      end)
-
-      assert length(Repo.all(SubscriptionNotification)) == 2
-    end
-  end
-
   describe "reblogging" do
     test "reblogs and returns the reblogged status", %{conn: conn} do
       activity = insert(:note_activity)
diff --git a/test/web/pleroma_api/subscription_notification_controller_test.exs b/test/web/pleroma_api/subscription_notification_controller_test.exs
new file mode 100644 (file)
index 0000000..ee495f1
--- /dev/null
@@ -0,0 +1,234 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.SubscriptionNotificationControllerTest do
+  use Pleroma.Web.ConnCase
+
+  alias Pleroma.Repo
+  alias Pleroma.SubscriptionNotification
+  alias Pleroma.User
+  alias Pleroma.Web.CommonAPI
+  import Pleroma.Factory
+  import Tesla.Mock
+
+  setup do
+    mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+    :ok
+  end
+
+  clear_config([:instance, :public])
+  clear_config([:rich_media, :enabled])
+
+  describe "subscription_notifications" do
+    setup do
+      user = insert(:user)
+      subscriber = insert(:user)
+
+      User.subscribe(subscriber, user)
+
+      {:ok, %{user: user, subscriber: subscriber}}
+    end
+
+    test "list of notifications", %{conn: conn, user: user, subscriber: subscriber} do
+      status_text = "Hello"
+      {:ok, _activity} = CommonAPI.post(user, %{"status" => status_text})
+      path = subscription_notification_path(conn, :list)
+
+      conn =
+        conn
+        |> assign(:user, subscriber)
+        |> get(path)
+
+      assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200)
+      assert response == status_text
+    end
+
+    test "getting a single notification", %{conn: conn, user: user, subscriber: subscriber} do
+      status_text = "Hello"
+
+      {:ok, _activity} = CommonAPI.post(user, %{"status" => status_text})
+      [notification] = Repo.all(SubscriptionNotification)
+
+      path = subscription_notification_path(conn, :get, id: notification.id)
+
+      conn =
+        conn
+        |> assign(:user, subscriber)
+        |> get(path)
+
+      assert %{"status" => %{"content" => response}} = json_response(conn, 200)
+      assert response == status_text
+    end
+
+    test "dismissing a single notification also deletes it", %{
+      conn: conn,
+      user: user,
+      subscriber: subscriber
+    } do
+      status_text = "Hello"
+      {:ok, _activity} = CommonAPI.post(user, %{"status" => status_text})
+
+      [notification] = Repo.all(SubscriptionNotification)
+
+      conn =
+        conn
+        |> assign(:user, subscriber)
+        |> post(subscription_notification_path(conn, :dismiss), %{"id" => notification.id})
+
+      assert %{} = json_response(conn, 200)
+
+      assert Repo.all(SubscriptionNotification) == []
+    end
+
+    test "clearing all notifications also deletes them", %{
+      conn: conn,
+      user: user,
+      subscriber: subscriber
+    } do
+      status_text1 = "Hello"
+      status_text2 = "Hello again"
+      {:ok, _activity1} = CommonAPI.post(user, %{"status" => status_text1})
+      {:ok, _activity2} = CommonAPI.post(user, %{"status" => status_text2})
+
+      conn =
+        conn
+        |> assign(:user, subscriber)
+        |> post(subscription_notification_path(conn, :clear))
+
+      assert %{} = json_response(conn, 200)
+
+      conn =
+        build_conn()
+        |> assign(:user, subscriber)
+        |> get(subscription_notification_path(conn, :list))
+
+      assert json_response(conn, 200) == []
+
+      assert Repo.all(SubscriptionNotification) == []
+    end
+
+    test "paginates notifications using min_id, since_id, max_id, and limit", %{
+      conn: conn,
+      user: user,
+      subscriber: subscriber
+    } do
+      {:ok, activity1} = CommonAPI.post(user, %{"status" => "Hello 1"})
+      {:ok, activity2} = CommonAPI.post(user, %{"status" => "Hello 2"})
+      {:ok, activity3} = CommonAPI.post(user, %{"status" => "Hello 3"})
+      {:ok, activity4} = CommonAPI.post(user, %{"status" => "Hello 4"})
+
+      notification1_id =
+        Repo.get_by(SubscriptionNotification, activity_id: activity1.id).id |> to_string()
+
+      notification2_id =
+        Repo.get_by(SubscriptionNotification, activity_id: activity2.id).id |> to_string()
+
+      notification3_id =
+        Repo.get_by(SubscriptionNotification, activity_id: activity3.id).id |> to_string()
+
+      notification4_id =
+        Repo.get_by(SubscriptionNotification, activity_id: activity4.id).id |> to_string()
+
+      conn = assign(conn, :user, subscriber)
+
+      # min_id
+      conn_res =
+        get(
+          conn,
+          subscription_notification_path(conn, :list, %{
+            "limit" => 2,
+            "min_id" => notification1_id
+          })
+        )
+
+      result = json_response(conn_res, 200)
+      assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
+
+      # since_id
+      conn_res =
+        get(
+          conn,
+          subscription_notification_path(conn, :list, %{
+            "limit" => 2,
+            "since_id" => notification1_id
+          })
+        )
+
+      result = json_response(conn_res, 200)
+      assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
+
+      # max_id
+      conn_res =
+        get(
+          conn,
+          subscription_notification_path(conn, :list, %{
+            "limit" => 2,
+            "max_id" => notification4_id
+          })
+        )
+
+      result = json_response(conn_res, 200)
+      assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
+    end
+
+    test "destroy multiple", %{conn: conn, user: user1, subscriber: user2} do
+      # mutual subscription
+      User.subscribe(user1, user2)
+
+      {:ok, activity1} = CommonAPI.post(user1, %{"status" => "Hello 1"})
+      {:ok, activity2} = CommonAPI.post(user1, %{"status" => "World 1"})
+      {:ok, activity3} = CommonAPI.post(user2, %{"status" => "Hello 2"})
+      {:ok, activity4} = CommonAPI.post(user2, %{"status" => "World 2"})
+
+      notification1_id =
+        Repo.get_by(SubscriptionNotification, activity_id: activity1.id).id |> to_string()
+
+      notification2_id =
+        Repo.get_by(SubscriptionNotification, activity_id: activity2.id).id |> to_string()
+
+      notification3_id =
+        Repo.get_by(SubscriptionNotification, activity_id: activity3.id).id |> to_string()
+
+      notification4_id =
+        Repo.get_by(SubscriptionNotification, activity_id: activity4.id).id |> to_string()
+
+      conn = assign(conn, :user, user1)
+
+      conn_res = get(conn, subscription_notification_path(conn, :list))
+
+      result = json_response(conn_res, 200)
+
+      Enum.each(result, fn %{"id" => id} ->
+        assert id in [notification3_id, notification4_id]
+      end)
+
+      conn2 = assign(conn, :user, user2)
+
+      conn_res = get(conn2, subscription_notification_path(conn, :list))
+
+      result = json_response(conn_res, 200)
+
+      Enum.each(result, fn %{"id" => id} ->
+        assert id in [notification1_id, notification2_id]
+      end)
+
+      conn_destroy =
+        delete(conn, subscription_notification_path(conn, :destroy_multiple), %{
+          "ids" => [notification3_id, notification4_id]
+        })
+
+      assert json_response(conn_destroy, 200) == %{}
+
+      conn_res = get(conn2, subscription_notification_path(conn, :list))
+
+      result = json_response(conn_res, 200)
+
+      Enum.each(result, fn %{"id" => id} ->
+        assert id in [notification1_id, notification2_id]
+      end)
+
+      assert length(Repo.all(SubscriptionNotification)) == 2
+    end
+  end
+end