From bf25d227861c2f6e5914379f981f32436372807d Mon Sep 17 00:00:00 2001
From: Thomas Citharel <tcit@tcit.fr>
Date: Fri, 11 Oct 2019 11:50:06 +0200
Subject: [PATCH] Make sure people can't join an event with limited
 participants

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
---
 lib/mobilizon/events/events.ex                | 16 +++++
 lib/mobilizon_web/resolvers/event.ex          |  3 +
 lib/service/activity_pub/activity_pub.ex      | 12 +++-
 .../resolvers/participant_resolver_test.exs   | 70 +++++++++++++++++++
 test/support/factory.ex                       |  3 +-
 5 files changed, 101 insertions(+), 3 deletions(-)

diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex
index 3bae79e1a..739c65a41 100644
--- a/lib/mobilizon/events/events.ex
+++ b/lib/mobilizon/events/events.ex
@@ -762,6 +762,17 @@ defmodule Mobilizon.Events do
     |> Repo.aggregate(:count, :id)
   end
 
+  @doc """
+  Counts participant participants.
+  """
+  @spec count_participant_participants(integer | String.t()) :: integer
+  def count_participant_participants(event_id) do
+    event_id
+    |> count_participants_query()
+    |> filter_participant_role()
+    |> Repo.aggregate(:count, :id)
+  end
+
   @doc """
   Counts unapproved participants.
   """
@@ -1457,6 +1468,11 @@ defmodule Mobilizon.Events do
     from(p in query, where: p.role not in ^[:not_approved, :rejected])
   end
 
+  @spec filter_participant_role(Ecto.Query.t()) :: Ecto.Query.t()
+  defp filter_participant_role(query) do
+    from(p in query, where: p.role == ^:participant)
+  end
+
   @spec filter_unapproved_role(Ecto.Query.t()) :: Ecto.Query.t()
   defp filter_unapproved_role(query) do
     from(p in query, where: p.role == ^:not_approved)
diff --git a/lib/mobilizon_web/resolvers/event.ex b/lib/mobilizon_web/resolvers/event.ex
index 9fb155bf3..7105e628f 100644
--- a/lib/mobilizon_web/resolvers/event.ex
+++ b/lib/mobilizon_web/resolvers/event.ex
@@ -170,6 +170,9 @@ defmodule MobilizonWeb.Resolvers.Event do
            |> Map.put(:actor, Person.proxify_pictures(actor)) do
       {:ok, participant}
     else
+      {:maximum_attendee_capacity, _} ->
+        {:error, "The event has already reached it's maximum capacity"}
+
       {:has_event, _} ->
         {:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
 
diff --git a/lib/service/activity_pub/activity_pub.ex b/lib/service/activity_pub/activity_pub.ex
index b98753c06..f3de44b05 100644
--- a/lib/service/activity_pub/activity_pub.ex
+++ b/lib/service/activity_pub/activity_pub.ex
@@ -411,8 +411,16 @@ defmodule Mobilizon.Service.ActivityPub do
 
   def join(object, actor, local \\ true)
 
-  def join(%Event{} = event, %Actor{} = actor, local) do
-    with role <- Mobilizon.Events.get_default_participant_role(event),
+  def join(%Event{options: options} = event, %Actor{} = actor, local) do
+    # TODO Refactor me for federation
+    with maximum_attendee_capacity <-
+           Map.get(options, :maximum_attendee_capacity, 2_000_000) || false,
+         {:maximum_attendee_capacity, true} <-
+           {:maximum_attendee_capacity,
+            !maximum_attendee_capacity ||
+              Mobilizon.Events.count_participant_participants(event.id) <
+                maximum_attendee_capacity},
+         role <- Mobilizon.Events.get_default_participant_role(event),
          {:ok, %Participant{} = participant} <-
            Mobilizon.Events.create_participant(%{
              role: role,
diff --git a/test/mobilizon_web/resolvers/participant_resolver_test.exs b/test/mobilizon_web/resolvers/participant_resolver_test.exs
index c0e69ea2f..bd195d47c 100644
--- a/test/mobilizon_web/resolvers/participant_resolver_test.exs
+++ b/test/mobilizon_web/resolvers/participant_resolver_test.exs
@@ -74,6 +74,76 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
       assert hd(json_response(res, 200)["errors"])["message"] =~ "already a participant"
     end
 
+    test "actor_join_event/3 doesn't work if the event already has too much participants", %{
+      conn: conn,
+      actor: actor
+    } do
+      event = insert(:event, options: %{maximum_attendee_capacity: 2})
+      insert(:participant, event: event, actor: actor, role: :creator)
+      insert(:participant, event: event, role: :participant)
+      insert(:participant, event: event, role: :not_approved)
+      insert(:participant, event: event, role: :rejected)
+      user_participant = insert(:user)
+      actor_participant = insert(:actor, user: user_participant)
+
+      mutation = """
+          mutation {
+            joinEvent(
+              actor_id: #{actor_participant.id},
+              event_id: #{event.id}
+            ) {
+                role,
+                actor {
+                  id
+                },
+                event {
+                  id
+                }
+              }
+            }
+      """
+
+      res =
+        conn
+        |> auth_conn(user_participant)
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert json_response(res, 200)["errors"] == nil
+      assert json_response(res, 200)["data"]["joinEvent"]["role"] == "PARTICIPANT"
+      assert json_response(res, 200)["data"]["joinEvent"]["event"]["id"] == to_string(event.id)
+
+      assert json_response(res, 200)["data"]["joinEvent"]["actor"]["id"] ==
+               to_string(actor_participant.id)
+
+      user_participant_2 = insert(:user)
+      actor_participant_2 = insert(:actor, user: user_participant_2)
+
+      mutation = """
+          mutation {
+            joinEvent(
+              actor_id: #{actor_participant_2.id},
+              event_id: #{event.id}
+            ) {
+                role,
+                actor {
+                  id
+                },
+                event {
+                  id
+                }
+              }
+            }
+      """
+
+      res =
+        conn
+        |> auth_conn(user_participant_2)
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert hd(json_response(res, 200)["errors"])["message"] ==
+               "The event has already reached it's maximum capacity"
+    end
+
     test "actor_join_event/3 should check the actor is owned by the user", %{
       conn: conn,
       user: user
diff --git a/test/support/factory.ex b/test/support/factory.ex
index c866ca6c4..7e7c8597f 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -124,7 +124,8 @@ defmodule Mobilizon.Factory do
       url: Routes.page_url(Endpoint, :event, uuid),
       picture: insert(:picture),
       uuid: uuid,
-      join_options: :free
+      join_options: :free,
+      options: %{}
     }
   end