diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex
index d7bbe8498ebdeeaaa89bae7332378d2fb58b67d6..b3709829f4235f03e33d4e2cbfa52f6b89b500ab 100644
--- a/lib/mobilizon/actors/actor.ex
+++ b/lib/mobilizon/actors/actor.ex
@@ -8,7 +8,7 @@ defenum(Mobilizon.Actors.ActorTypeEnum, :actor_type, [
   :Service
 ])
 
-defenum(Mobilizon.Actors.ActorOpennesssEnum, :openness, [
+defenum(Mobilizon.Actors.ActorOpennessEnum, :actor_openness, [
   :invite_only,
   :moderated,
   :open
@@ -48,6 +48,7 @@ defmodule Mobilizon.Actors.Actor do
     field(:preferred_username, :string)
     field(:keys, :string)
     field(:manually_approves_followers, :boolean, default: false)
+    field(:openness, Mobilizon.Actors.ActorOpennessEnum, default: :moderated)
     field(:suspended, :boolean, default: false)
     field(:avatar_url, :string)
     field(:banner_url, :string)
diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex
index e58a8e8490698feb5a88fefeb80997d42e756919..e1ad14461758d4123794868cbe3536b3ae73c8a7 100644
--- a/lib/mobilizon/actors/actors.ex
+++ b/lib/mobilizon/actors/actors.ex
@@ -162,6 +162,12 @@ defmodule Mobilizon.Actors do
     )
   end
 
+  @doc """
+  Get the default member role depending on the actor openness
+  """
+  def get_default_member_role(%Actor{openness: :open}), do: :member
+  def get_default_member_role(%Actor{}), do: :not_approved
+
   @doc """
   Get a group by it's title
   """
diff --git a/lib/mobilizon/actors/member.ex b/lib/mobilizon/actors/member.ex
index 5d6b85075029dad20fa592336abea4c84b648377..6580a69dff9b2836ddd052c76a4a604db0c1da03 100644
--- a/lib/mobilizon/actors/member.ex
+++ b/lib/mobilizon/actors/member.ex
@@ -1,17 +1,29 @@
+import EctoEnum
+
+defenum(Mobilizon.Actors.MemberRoleEnum, :member_role_type, [
+  :not_approved,
+  :member,
+  :moderator,
+  :administrator,
+  :creator
+])
+
 defmodule Mobilizon.Actors.Member do
   @moduledoc """
   Represents the membership of an actor to a group
   """
   use Ecto.Schema
+
   import Ecto.Changeset
+  import Ecto.Query, warn: false
+  import Mobilizon.Ecto
+
   alias Mobilizon.Actors.Member
   alias Mobilizon.Actors.Actor
   alias Mobilizon.Repo
 
   schema "members" do
-    field(:approved, :boolean, default: true)
-    # 0 : Member, 1 : Moderator, 2 : Admin
-    field(:role, :integer, default: 0)
+    field(:role, Mobilizon.Actors.MemberRoleEnum, default: :member)
     belongs_to(:parent, Actor)
     belongs_to(:actor, Actor)
 
@@ -21,7 +33,7 @@ defmodule Mobilizon.Actors.Member do
   @doc false
   def changeset(%Member{} = member, attrs) do
     member
-    |> cast(attrs, [:role, :approved, :parent_id, :actor_id])
+    |> cast(attrs, [:role, :parent_id, :actor_id])
     |> validate_required([:parent_id, :actor_id])
     |> unique_constraint(:parent_id, name: :members_actor_parent_unique_index)
   end
@@ -36,11 +48,27 @@ defmodule Mobilizon.Actors.Member do
     end
   end
 
-  def is_administrator(%Member{role: 2}) do
-    {:is_admin, true}
-  end
+  @doc """
+  Gets a single member of an actor (for example a group)
+  """
+  def can_be_joined(%Actor{type: :Group, openness: :invite_only}), do: false
+  def can_be_joined(%Actor{type: :Group}), do: true
 
-  def is_administrator(%Member{}) do
-    {:is_admin, false}
+  @doc """
+  Returns the list of administrator members for a group.
+  """
+  def list_administrator_members_for_group(id, page \\ nil, limit \\ nil) do
+    Repo.all(
+      from(
+        m in Member,
+        where: m.parent_id == ^id and (m.role == ^:creator or m.role == ^:administrator),
+        preload: [:actor]
+      )
+      |> paginate(page, limit)
+    )
   end
+
+  def is_administrator(%Member{role: :administrator}), do: {:is_admin, true}
+  def is_administrator(%Member{role: :creator}), do: {:is_admin, true}
+  def is_administrator(%Member{}), do: {:is_admin, false}
 end
diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex
index 1855435f338a9afc9ce3f23bb0e411a06da7c8e7..61263efc6c32bce438393c047ce67367156676b1 100644
--- a/lib/mobilizon/events/events.ex
+++ b/lib/mobilizon/events/events.ex
@@ -5,8 +5,8 @@ defmodule Mobilizon.Events do
 
   import Ecto.Query, warn: false
   import Mobilizon.Ecto
-  alias Mobilizon.Repo
 
+  alias Mobilizon.Repo
   alias Mobilizon.Events.{Event, Comment, Participant}
   alias Mobilizon.Actors.Actor
   alias Mobilizon.Addresses.Address
@@ -607,9 +607,7 @@ defmodule Mobilizon.Events do
     Repo.all(
       from(
         p in Participant,
-        join: e in Event,
-        on: p.event_id == e.id,
-        where: e.id == ^id and p.role == ^:creator,
+        where: p.event_id == ^id and p.role == ^:creator,
         preload: [:actor]
       )
       |> paginate(page, limit)
diff --git a/lib/mobilizon_web/resolvers/event.ex b/lib/mobilizon_web/resolvers/event.ex
index b1ce90944eb17bc689aa93e7c225be67e153fa3b..ac11e1b12b982c1c2ea2af2ed5ff2f3f8b8a1a36 100644
--- a/lib/mobilizon_web/resolvers/event.ex
+++ b/lib/mobilizon_web/resolvers/event.ex
@@ -32,8 +32,8 @@ defmodule MobilizonWeb.Resolvers.Event do
   @doc """
   List participant for event (separate request)
   """
-  def list_participants_for_event(_parent, %{uuid: uuid}, _resolution) do
-    {:ok, Mobilizon.Events.list_participants_for_event(uuid)}
+  def list_participants_for_event(_parent, %{uuid: uuid, page: page, limit: limit}, _resolution) do
+    {:ok, Mobilizon.Events.list_participants_for_event(uuid, page, limit)}
   end
 
   @doc """
diff --git a/lib/mobilizon_web/resolvers/group.ex b/lib/mobilizon_web/resolvers/group.ex
index f104e3d27f0c4e012f95d43a2492e9c5954105dc..edd199e67a5faf5fbe595915c03afdf044bd63a4 100644
--- a/lib/mobilizon_web/resolvers/group.ex
+++ b/lib/mobilizon_web/resolvers/group.ex
@@ -36,19 +36,30 @@ defmodule MobilizonWeb.Resolvers.Group do
         _parent,
         args,
         %{
-          context: %{current_user: _user}
+          context: %{
+            current_user: _user
+          }
         }
       ) do
-    with {:ok, %Activity{data: %{"object" => %{"type" => "Group"} = object}}} <-
+    with {
+           :ok,
+           %Activity{
+             data: %{
+               "object" => %{"type" => "Group"} = object
+             }
+           }
+         } <-
            MobilizonWeb.API.Groups.create_group(args) do
-      {:ok,
-       %Actor{
-         preferred_username: object["preferredUsername"],
-         summary: object["summary"],
-         type: :Group,
-         #  uuid: object["uuid"],
-         url: object["id"]
-       }}
+      {
+        :ok,
+        %Actor{
+          preferred_username: object["preferredUsername"],
+          summary: object["summary"],
+          type: :Group,
+          #  uuid: object["uuid"],
+          url: object["id"]
+        }
+      }
     end
 
     # with %Actor{id: actor_id} <- Actors.get_local_actor_by_name(actor_username),
@@ -106,4 +117,111 @@ defmodule MobilizonWeb.Resolvers.Group do
   def delete_group(_parent, _args, _resolution) do
     {:error, "You need to be logged-in to delete a group"}
   end
+
+  @doc """
+  Join an existing group
+  """
+  def join_group(
+        _parent,
+        %{group_id: group_id, actor_id: actor_id},
+        %{
+          context: %{
+            current_user: user
+          }
+        }
+      ) do
+    with {:is_owned, true, actor} <- User.owns_actor(user, actor_id),
+         {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
+         {:error, :member_not_found} <- Member.get_member(actor.id, group.id),
+         {:is_able_to_join, true} <- {:is_able_to_join, Member.can_be_joined(group)},
+         role <- Mobilizon.Actors.get_default_member_role(group),
+         {:ok, _} <-
+           Actors.create_member(%{
+             parent_id: group.id,
+             actor_id: actor.id,
+             role: role
+           }) do
+      {:ok, %{parent: group, person: actor, role: role}}
+    else
+      {:is_owned, false} ->
+        {:error, "Actor id is not owned by authenticated user"}
+
+      {:error, :group_not_found} ->
+        {:error, "Group id not found"}
+
+      {:is_able_to_join, false} ->
+        {:error, "You cannot join this group"}
+
+      {:ok, %Member{}} ->
+        {:error, "You are already a member of this group"}
+    end
+  end
+
+  def join_group(_parent, _args, _resolution) do
+    {:error, "You need to be logged-in to join a group"}
+  end
+
+  @doc """
+  Leave a existing group
+  """
+  def leave_group(
+        _parent,
+        %{group_id: group_id, actor_id: actor_id},
+        %{
+          context: %{
+            current_user: user
+          }
+        }
+      ) do
+    with {:is_owned, true, actor} <- User.owns_actor(user, actor_id),
+         {:ok, %Member{} = member} <- Member.get_member(actor.id, group_id),
+         {:only_administrator, false} <-
+           {:only_administrator, check_that_member_is_not_only_administrator(group_id, actor_id)},
+         {:ok, _} <-
+           Mobilizon.Actors.delete_member(member) do
+      {
+        :ok,
+        %{
+          parent: %{
+            id: group_id
+          },
+          person: %{
+            id: actor_id
+          }
+        }
+      }
+    else
+      {:is_owned, false} ->
+        {:error, "Actor id is not owned by authenticated user"}
+
+      {:error, :member_not_found} ->
+        {:error, "Member not found"}
+
+      {:only_administrator, true} ->
+        {:error, "You can't leave this group because you are the only administrator"}
+    end
+  end
+
+  def leave_group(_parent, _args, _resolution) do
+    {:error, "You need to be logged-in to leave a group"}
+  end
+
+  # We check that the actor asking to leave the group is not it's only administrator
+  # We start by fetching the list of administrator or creators and if there's only one of them
+  # and that it's the actor requesting leaving the group we return true
+  @spec check_that_member_is_not_only_administrator(integer(), integer()) :: boolean()
+  defp check_that_member_is_not_only_administrator(group_id, actor_id) do
+    with [
+           %Member{
+             actor: %Actor{
+               id: member_actor_id
+             }
+           }
+         ] <-
+           Member.list_administrator_members_for_group(group_id) do
+      actor_id == member_actor_id
+    else
+      _ -> false
+    end
+  end
 end
diff --git a/lib/mobilizon_web/schema.ex b/lib/mobilizon_web/schema.ex
index 7d6ff35aee887d94bb22899e259a4646db3db203..65cebddc9700a07a6f91f9f737f47557df010d72 100644
--- a/lib/mobilizon_web/schema.ex
+++ b/lib/mobilizon_web/schema.ex
@@ -143,6 +143,7 @@ defmodule MobilizonWeb.Schema do
     import_fields(:event_mutations)
     import_fields(:comment_mutations)
     import_fields(:participant_mutations)
+    import_fields(:member_mutations)
 
     # @desc "Upload a picture"
     # field :upload_picture, :picture do
diff --git a/lib/mobilizon_web/schema/actors/member.ex b/lib/mobilizon_web/schema/actors/member.ex
index 26848f9142bee81ece79ebd7129a6465b7b37348..86e2f624b9f241928f1e6b6a6237b7d90391c689 100644
--- a/lib/mobilizon_web/schema/actors/member.ex
+++ b/lib/mobilizon_web/schema/actors/member.ex
@@ -4,6 +4,8 @@ defmodule MobilizonWeb.Schema.Actors.MemberType do
   """
   use Absinthe.Schema.Notation
 
+  alias MobilizonWeb.Resolvers
+
   @desc """
   Represents a member of a group
   """
@@ -11,6 +13,29 @@ defmodule MobilizonWeb.Schema.Actors.MemberType do
     field(:parent, :group, description: "Of which the profile is member")
     field(:person, :person, description: "Which profile is member of")
     field(:role, :integer, description: "The role of this membership")
-    field(:approved, :boolean, description: "Whether this membership has been approved")
+  end
+
+  @desc "Represents a deleted member"
+  object :deleted_member do
+    field(:parent, :deleted_object)
+    field(:person, :deleted_object)
+  end
+
+  object :member_mutations do
+    @desc "Join a group"
+    field :join_group, :member do
+      arg(:group_id, non_null(:integer))
+      arg(:actor_id, non_null(:integer))
+
+      resolve(&Resolvers.Group.join_group/3)
+    end
+
+    @desc "Leave an event"
+    field :leave_group, :deleted_member do
+      arg(:group_id, non_null(:integer))
+      arg(:actor_id, non_null(:integer))
+
+      resolve(&Resolvers.Group.leave_group/3)
+    end
   end
 end
diff --git a/priv/repo/migrations/20190301141830_move_member_role_to_enum.exs b/priv/repo/migrations/20190301141830_move_member_role_to_enum.exs
new file mode 100644
index 0000000000000000000000000000000000000000..b0e8dfea176f64e81646a3430ffe0321065a312e
--- /dev/null
+++ b/priv/repo/migrations/20190301141830_move_member_role_to_enum.exs
@@ -0,0 +1,46 @@
+defmodule Mobilizon.Repo.Migrations.MoveMemberRoleToEnum do
+  use Ecto.Migration
+  alias Mobilizon.Actors.MemberRoleEnum
+
+  def up do
+    MemberRoleEnum.create_type()
+
+    alter table(:members) do
+      add(:role_tmp, MemberRoleEnum.type(), default: "member")
+    end
+
+    execute("UPDATE members set role_tmp = 'member' where role = 0")
+    execute("UPDATE members set role_tmp = 'moderator' where role = 1")
+    execute("UPDATE members set role_tmp = 'creator' where role = 2")
+
+    execute("UPDATE members set role_tmp = 'not_approved' where approved is false")
+
+    alter table(:members) do
+      remove(:role)
+      remove(:approved)
+    end
+
+    rename(table(:members), :role_tmp, to: :role)
+  end
+
+  def down do
+    alter table(:members) do
+      add(:role_tmp, :integer, default: 0)
+      add(:approved, :boolean, default: true)
+    end
+
+    execute("UPDATE members set approved = false where role = 'not_approved'")
+
+    execute("UPDATE members set role_tmp = 0 where role = 'member' or role = 'not_approved'")
+    execute("UPDATE members set role_tmp = 1 where role = 'moderator'")
+    execute("UPDATE members set role_tmp = 2 where role = 'administrator' or role = 'creator'")
+
+    alter table(:members) do
+      remove(:role)
+    end
+
+    MemberRoleEnum.drop_type()
+
+    rename(table(:members), :role_tmp, to: :role)
+  end
+end
diff --git a/priv/repo/migrations/20190301143831_actor_group_openness.exs b/priv/repo/migrations/20190301143831_actor_group_openness.exs
new file mode 100644
index 0000000000000000000000000000000000000000..34c5517346aa5db48ecf30db231491044b739c5f
--- /dev/null
+++ b/priv/repo/migrations/20190301143831_actor_group_openness.exs
@@ -0,0 +1,18 @@
+defmodule Mobilizon.Repo.Migrations.ActorGroupOpenness do
+  use Ecto.Migration
+  alias Mobilizon.Actors.ActorOpennessEnum
+
+  def up do
+    ActorOpennessEnum.create_type()
+
+    alter table(:actors) do
+      add(:openness, ActorOpennessEnum.type(), default: "moderated")
+    end
+  end
+
+  def down do
+    alter table(:actors) do
+      remove(:openness)
+    end
+  end
+end
diff --git a/test/mobilizon/actors/actors_test.exs b/test/mobilizon/actors/actors_test.exs
index 70192c11127d5b588544b18750d0a3cf03315ae9..4b79cf76cc7b14f409e8066e30d9ef12672f7a4b 100644
--- a/test/mobilizon/actors/actors_test.exs
+++ b/test/mobilizon/actors/actors_test.exs
@@ -499,9 +499,9 @@ defmodule Mobilizon.ActorsTest do
     alias Mobilizon.Actors.Member
     alias Mobilizon.Actors.Actor
 
-    @valid_attrs %{approved: true, role: 0}
-    @update_attrs %{approved: false, role: 1}
-    @invalid_attrs %{approved: nil, role: nil}
+    @valid_attrs %{role: :member}
+    @update_attrs %{role: :not_approved}
+    @invalid_attrs %{role: nil}
 
     setup do
       actor = insert(:actor)
@@ -528,8 +528,7 @@ defmodule Mobilizon.ActorsTest do
         |> Map.put(:parent_id, group.id)
 
       assert {:ok, %Member{} = member} = Actors.create_member(valid_attrs)
-      assert member.approved == true
-      assert member.role == 0
+      assert member.role == :member
 
       assert [group] = Actor.get_groups_member_of(actor)
       assert [actor] = Actor.get_members_for_group(group)
@@ -562,8 +561,7 @@ defmodule Mobilizon.ActorsTest do
       member = create_test_member(context)
       assert {:ok, member} = Actors.update_member(member, @update_attrs)
       assert %Member{} = member
-      assert member.approved == false
-      assert member.role == 1
+      assert member.role == :not_approved
     end
 
     # This can't happen, since attrs are optional
diff --git a/test/mobilizon_web/resolvers/group_resolver_test.exs b/test/mobilizon_web/resolvers/group_resolver_test.exs
index 24469aa8a9d5e8c9b650aefb6fedfa09fc5d34ad..8bfa255bea997b14fcaf379ced2cf11d1dc22364 100644
--- a/test/mobilizon_web/resolvers/group_resolver_test.exs
+++ b/test/mobilizon_web/resolvers/group_resolver_test.exs
@@ -115,7 +115,7 @@ defmodule MobilizonWeb.Resolvers.GroupResolverTest do
 
     test "delete_group/3 deletes a group", %{conn: conn, user: user, actor: actor} do
       group = insert(:group)
-      insert(:member, parent: group, actor: actor, role: 2)
+      insert(:member, parent: group, actor: actor, role: :administrator)
 
       mutation = """
           mutation {
@@ -146,7 +146,7 @@ defmodule MobilizonWeb.Resolvers.GroupResolverTest do
 
     test "delete_group/3 should check user authentication", %{conn: conn, actor: actor} do
       group = insert(:group)
-      insert(:member, parent: group, actor: actor, role: 2)
+      insert(:member, parent: group, actor: actor, role: :member)
 
       mutation = """
           mutation {
@@ -172,7 +172,7 @@ defmodule MobilizonWeb.Resolvers.GroupResolverTest do
       actor: actor
     } do
       group = insert(:group)
-      insert(:member, parent: group, actor: actor, role: 2)
+      insert(:member, parent: group, actor: actor, role: :member)
 
       mutation = """
           mutation {
@@ -225,7 +225,7 @@ defmodule MobilizonWeb.Resolvers.GroupResolverTest do
       actor: actor
     } do
       group = insert(:group)
-      insert(:member, parent: group, actor: actor, role: 1)
+      insert(:member, parent: group, actor: actor, role: :member)
 
       mutation = """
           mutation {
diff --git a/test/mobilizon_web/resolvers/member_resolver_test.exs b/test/mobilizon_web/resolvers/member_resolver_test.exs
new file mode 100644
index 0000000000000000000000000000000000000000..fb075ed0dad325a6c7a2235b3a427f2d922736fa
--- /dev/null
+++ b/test/mobilizon_web/resolvers/member_resolver_test.exs
@@ -0,0 +1,289 @@
+defmodule MobilizonWeb.Resolvers.MemberResolverTest do
+  use MobilizonWeb.ConnCase
+
+  alias MobilizonWeb.AbsintheHelpers
+
+  import Mobilizon.Factory
+
+  setup %{conn: conn} do
+    user = insert(:user)
+    actor = insert(:actor, user: user, preferred_username: "test")
+
+    {:ok, conn: conn, actor: actor, user: user}
+  end
+
+  describe "Member Resolver" do
+    test "join_group/3 should create a member", %{conn: conn, user: user, actor: actor} do
+      group = insert(:group)
+
+      mutation = """
+          mutation {
+            joinGroup(
+              actor_id: #{actor.id},
+              group_id: #{group.id}
+            ) {
+                role,
+                person {
+                  id
+                },
+                parent {
+                  id
+                }
+              }
+            }
+      """
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert json_response(res, 200)["errors"] == nil
+      assert json_response(res, 200)["data"]["joinGroup"]["role"] == "not_approved"
+      assert json_response(res, 200)["data"]["joinGroup"]["parent"]["id"] == group.id
+      assert json_response(res, 200)["data"]["joinGroup"]["person"]["id"] == actor.id
+
+      mutation = """
+         mutation {
+            joinGroup(
+              actor_id: #{actor.id},
+              group_id: #{group.id}
+            ) {
+                role
+              }
+            }
+      """
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert hd(json_response(res, 200)["errors"])["message"] =~ "already a member"
+    end
+
+    test "join_group/3 should check the actor is owned by the user", %{
+      conn: conn,
+      user: user
+    } do
+      group = insert(:group)
+
+      mutation = """
+          mutation {
+            joinGroup(
+              actor_id: 1042,
+              group_id: #{group.id}
+            ) {
+                role
+              }
+            }
+      """
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert hd(json_response(res, 200)["errors"])["message"] =~ "not owned"
+    end
+
+    test "join_group/3 should check the group is not invite only", %{
+      conn: conn,
+      actor: actor,
+      user: user
+    } do
+      group = insert(:group, %{openness: :invite_only})
+
+      mutation = """
+          mutation {
+            joinGroup(
+              actor_id: #{actor.id},
+              group_id: #{group.id}
+            ) {
+                role
+              }
+            }
+      """
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert hd(json_response(res, 200)["errors"])["message"] =~ "cannot join this group"
+    end
+
+    test "join_group/3 should check the group exists", %{
+      conn: conn,
+      user: user,
+      actor: actor
+    } do
+      mutation = """
+          mutation {
+            joinGroup(
+              actor_id: #{actor.id},
+              group_id: 1042
+            ) {
+                role
+              }
+            }
+      """
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert hd(json_response(res, 200)["errors"])["message"] =~ "Group id not found"
+    end
+
+    test "leave_group/3 should delete a member from a group", %{
+      conn: conn,
+      user: user,
+      actor: actor
+    } do
+      group = insert(:group)
+      insert(:member, %{actor: actor, parent: group})
+
+      mutation = """
+          mutation {
+            leaveGroup(
+              actor_id: #{actor.id},
+              group_id: #{group.id}
+            ) {
+                person {
+                  id
+                },
+                parent {
+                  id
+                }
+              }
+            }
+      """
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert json_response(res, 200)["errors"] == nil
+      assert json_response(res, 200)["data"]["leaveGroup"]["parent"]["id"] == group.id
+      assert json_response(res, 200)["data"]["leaveGroup"]["person"]["id"] == actor.id
+    end
+
+    test "leave_group/3 should check if the member is the only administrator", %{
+      conn: conn,
+      actor: actor,
+      user: user
+    } do
+      group = insert(:group)
+      insert(:member, %{actor: actor, role: :creator, parent: group})
+      insert(:member, %{parent: group})
+
+      mutation = """
+          mutation {
+            leaveGroup(
+              actor_id: #{actor.id},
+              group_id: #{group.id}
+            ) {
+                person {
+                  id
+                },
+                parent {
+                  id
+                }
+              }
+            }
+      """
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert hd(json_response(res, 200)["errors"])["message"] =~ "only administrator"
+    end
+
+    test "leave_group/3 should check the user is logged in", %{conn: conn, actor: actor} do
+      group = insert(:group)
+      insert(:member, %{actor: actor, parent: group})
+
+      mutation = """
+          mutation {
+            leaveGroup(
+              actor_id: #{actor.id},
+              group_id: #{group.id}
+            ) {
+                person {
+                  id
+                }
+              }
+            }
+      """
+
+      res =
+        conn
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert hd(json_response(res, 200)["errors"])["message"] =~ "logged-in"
+    end
+
+    test "leave_group/3 should check the actor is owned by the user", %{
+      conn: conn,
+      user: user,
+      actor: actor
+    } do
+      group = insert(:group)
+      insert(:member, %{actor: actor, parent: group})
+
+      mutation = """
+          mutation {
+            leaveGroup(
+              actor_id: 1042,
+              group_id: #{group.id}
+            ) {
+                person {
+                  id
+                }
+              }
+            }
+      """
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert hd(json_response(res, 200)["errors"])["message"] =~ "not owned"
+    end
+
+    test "leave_group/3 should check the member exists", %{
+      conn: conn,
+      user: user,
+      actor: actor
+    } do
+      group = insert(:group)
+      insert(:member, %{actor: actor, parent: group})
+
+      mutation = """
+          mutation {
+            leaveGroup(
+              actor_id: #{actor.id},
+              group_id: 1042
+            ) {
+                person {
+                  id
+                }
+              }
+            }
+      """
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert hd(json_response(res, 200)["errors"])["message"] =~ "Member not found"
+    end
+  end
+end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 57f36467736f9c3bc99cc547be17f42a5d798124..d34e9d0b96a7941bd495647e00253862bc48dae4 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -149,7 +149,7 @@ defmodule Mobilizon.Factory do
     %Mobilizon.Actors.Member{
       parent: build(:actor),
       actor: build(:actor),
-      role: 0
+      role: :not_approved
     }
   end
 end