diff --git a/js/src/graphql/member.ts b/js/src/graphql/member.ts index fe2e704bf..59e5cf23c 100644 --- a/js/src/graphql/member.ts +++ b/js/src/graphql/member.ts @@ -81,6 +81,15 @@ export const GROUP_MEMBERS = gql` } `; +export const UPDATE_MEMBER = gql` + mutation UpdateMember($memberId: ID!, $role: MemberRoleEnum!) { + updateMember(memberId: $memberId, role: $role) { + id + role + } + } +`; + export const REMOVE_MEMBER = gql` mutation RemoveMember($groupId: ID!, $memberId: ID!) { removeMember(groupId: $groupId, memberId: $memberId) { diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index 2eca592ef..9da515b2e 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -764,5 +764,7 @@ "Update": "Update", "Search…": "Search…", "Edited {ago}": "Edited {ago}", - "[This comment has been deleted by it's author]": "[This comment has been deleted by it's author]" + "[This comment has been deleted by it's author]": "[This comment has been deleted by it's author]", + "Promote": "Promote", + "Demote": "Demote" } diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index e8442adf8..9c2a07a26 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -765,5 +765,7 @@ "Update": "Éditer", "Search…": "Rechercher…", "Edited {ago}": "Édité {ago}", - "[This comment has been deleted by it's author]": "[Ce commentaire a été supprimé par son auteur]" + "[This comment has been deleted by it's author]": "[Ce commentaire a été supprimé par son auteur]", + "Promote": "Promouvoir", + "Demote": "Rétrograder" } diff --git a/js/src/views/Group/GroupMembers.vue b/js/src/views/Group/GroupMembers.vue index 9996a9444..e6fa34c04 100644 --- a/js/src/views/Group/GroupMembers.vue +++ b/js/src/views/Group/GroupMembers.vue @@ -134,12 +134,24 @@ - {{ $t("Remove") }} + + {{ $t("Promote") }} + {{ $t("Demote") }} + {{ $t("Remove") }} + @@ -156,7 +168,7 @@ diff --git a/lib/federation/activity_pub/activity_pub.ex b/lib/federation/activity_pub/activity_pub.ex index 95dae1fdc..4afd7e8c5 100644 --- a/lib/federation/activity_pub/activity_pub.ex +++ b/lib/federation/activity_pub/activity_pub.ex @@ -87,6 +87,7 @@ defmodule Mobilizon.Federation.ActivityPub do {:existing, nil} <- {:existing, Resources.get_resource_by_url(url)}, {:existing, nil} <- {:existing, Actors.get_actor_by_url_2(url)}, + {:existing, nil} <- {:existing, Actors.get_member_by_url(url)}, :ok <- Logger.info("Data for URL not found anywhere, going to fetch it"), {:ok, _activity, entity} <- Fetcher.fetch_and_create(url, options) do Logger.debug("Going to preload the new entity") @@ -359,16 +360,12 @@ defmodule Mobilizon.Federation.ActivityPub do end def join_group( - %{parent_id: parent_id, actor_id: actor_id, role: role}, + %{parent_id: _parent_id, actor_id: _actor_id, role: _role} = args, local \\ true, additional \\ %{} ) do with {:ok, %Member{} = member} <- - Mobilizon.Actors.create_member(%{ - parent_id: parent_id, - actor_id: actor_id, - role: role - }), + Mobilizon.Actors.create_member(args), activity_data when is_map(activity_data) <- Convertible.model_to_as(member), {:ok, activity} <- create_activity(Map.merge(activity_data, additional), local), diff --git a/lib/federation/activity_pub/fetcher.ex b/lib/federation/activity_pub/fetcher.ex index 5d3736763..256b65c97 100644 --- a/lib/federation/activity_pub/fetcher.ex +++ b/lib/federation/activity_pub/fetcher.ex @@ -33,8 +33,6 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do @spec fetch_and_create(String.t(), Keyword.t()) :: {:ok, map(), struct()} def fetch_and_create(url, options \\ []) do with {:ok, data} when is_map(data) <- fetch(url, options), - :ok <- Logger.debug("inspect body from fetch_object_from_url #{url}"), - :ok <- Logger.debug(inspect(data)), {:origin_check, true} <- {:origin_check, origin_check?(url, data)}, params <- %{ "type" => "Create", @@ -55,8 +53,6 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do @spec fetch_and_update(String.t(), Keyword.t()) :: {:ok, map(), struct()} def fetch_and_update(url, options \\ []) do with {:ok, data} when is_map(data) <- fetch(url, options), - :ok <- Logger.debug("inspect body from fetch_object_from_url #{url}"), - :ok <- Logger.debug(inspect(data)), {:origin_check, true} <- {:origin_check, origin_check?(url, data)}, params <- %{ "type" => "Update", diff --git a/lib/federation/activity_pub/preloader.ex b/lib/federation/activity_pub/preloader.ex index 6262eb171..dedcb0971 100644 --- a/lib/federation/activity_pub/preloader.ex +++ b/lib/federation/activity_pub/preloader.ex @@ -5,7 +5,7 @@ defmodule Mobilizon.Federation.ActivityPub.Preloader do # TODO: Move me in a more appropriate place alias Mobilizon.{Actors, Discussions, Events, Resources} - alias Mobilizon.Actors.Actor + alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Events.Event alias Mobilizon.Resources.Resource @@ -25,6 +25,8 @@ defmodule Mobilizon.Federation.ActivityPub.Preloader do def maybe_preload(%Actor{url: url}), do: {:ok, Actors.get_actor_by_url!(url, true)} + def maybe_preload(%Member{} = member), do: {:ok, member} + def maybe_preload(%Tombstone{uri: _uri} = tombstone), do: {:ok, tombstone} def maybe_preload(other), do: {:error, other} diff --git a/lib/federation/activity_pub/transmogrifier.ex b/lib/federation/activity_pub/transmogrifier.ex index caa6852f6..a82dfc48e 100644 --- a/lib/federation/activity_pub/transmogrifier.ex +++ b/lib/federation/activity_pub/transmogrifier.ex @@ -415,6 +415,27 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do end end + def handle_incoming( + %{"type" => "Update", "object" => %{"type" => "Member"} = object, "actor" => _actor} = + update_data + ) do + Logger.info("Handle incoming to update a member") + + with actor <- Utils.get_actor(update_data), + {:ok, %Actor{url: actor_url, suspended: false} = actor} <- + ActivityPub.get_or_fetch_actor_by_url(actor), + {:origin_check, true} <- {:origin_check, Utils.origin_check?(actor_url, update_data)}, + object_data <- Converter.Member.as_to_model_data(object), + {:ok, old_entity} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(), + {:ok, %Activity{} = activity, new_entity} <- + ActivityPub.update(old_entity, object_data, false, %{moderator: actor}) do + {:ok, activity, new_entity} + else + _e -> + :error + end + end + def handle_incoming(%{ "type" => "Update", "object" => %{"type" => "Tombstone"} = object, diff --git a/lib/federation/activity_pub/types/entity.ex b/lib/federation/activity_pub/types/entity.ex index f27565517..e0ec694fa 100644 --- a/lib/federation/activity_pub/types/entity.ex +++ b/lib/federation/activity_pub/types/entity.ex @@ -5,6 +5,7 @@ alias Mobilizon.Federation.ActivityPub.Types.{ Entity, Events, Managable, + Members, Ownable, Posts, Resources, @@ -13,7 +14,7 @@ alias Mobilizon.Federation.ActivityPub.Types.{ Tombstones } -alias Mobilizon.Actors.Actor +alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Events.Event alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Posts.Post @@ -149,3 +150,8 @@ defimpl Ownable, for: Tombstone do defdelegate group_actor(entity), to: Tombstones defdelegate actor(entity), to: Tombstones end + +defimpl Managable, for: Member do + defdelegate update(entity, attrs, additionnal), to: Members + defdelegate delete(entity, actor, local), to: Members +end diff --git a/lib/federation/activity_pub/types/members.ex b/lib/federation/activity_pub/types/members.ex new file mode 100644 index 000000000..98f6f1dad --- /dev/null +++ b/lib/federation/activity_pub/types/members.ex @@ -0,0 +1,54 @@ +defmodule Mobilizon.Federation.ActivityPub.Types.Members do + @moduledoc false + alias Mobilizon.Actors + alias Mobilizon.Actors.{Actor, Member} + alias Mobilizon.Federation.ActivityStream.Convertible + require Logger + import Mobilizon.Federation.ActivityPub.Utils, only: [make_update_data: 2] + + def update( + %Member{parent: %Actor{id: group_id}, id: member_id, role: current_role} = member, + %{role: updated_role} = args, + %{moderator: %Actor{url: moderator_url, id: moderator_id}} = additional + ) do + with additional <- Map.delete(additional, :moderator), + {:has_rights_to_update_role, {:ok, %Member{role: moderator_role}}} + when moderator_role in [:moderator, :administrator, :creator] <- + {:has_rights_to_update_role, Actors.get_member(moderator_id, group_id)}, + {:is_only_admin, false} <- + {:is_only_admin, check_admins_left(member_id, group_id, current_role, updated_role)}, + {:ok, %Member{} = member} <- + Actors.update_member(member, args), + {:ok, true} <- Cachex.del(:activity_pub, "member_#{member_id}"), + member_as_data <- + Convertible.model_to_as(member), + audience <- %{ + "to" => [member.parent.members_url, member.actor.url], + "cc" => [member.parent.url], + "actor" => moderator_url, + "attributedTo" => [member.parent.url] + } do + update_data = make_update_data(member_as_data, Map.merge(audience, additional)) + + {:ok, member, update_data} + else + err -> + Logger.debug(inspect(err)) + err + end + end + + # Delete member is not used, see ActivityPub.leave/4 and ActivityPub.remove/5 instead + def delete(_, _, _), do: :error + + def actor(%Member{actor_id: actor_id}), + do: Actors.get_actor(actor_id) + + def group_actor(%Member{parent_id: parent_id}), + do: Actors.get_actor(parent_id) + + defp check_admins_left(member_id, group_id, current_role, updated_role) do + Actors.is_only_administrator?(member_id, group_id) && current_role == :administrator && + updated_role != :administrator + end +end diff --git a/lib/federation/activity_stream/converter/member.ex b/lib/federation/activity_stream/converter/member.ex index c90c23241..c860fa3fc 100644 --- a/lib/federation/activity_stream/converter/member.ex +++ b/lib/federation/activity_stream/converter/member.ex @@ -29,7 +29,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Member do "id" => member.url, "actor" => member.actor.url, "object" => member.parent.url, - "role" => member.role + "role" => to_string(member.role) } end diff --git a/lib/graphql/resolvers/member.ex b/lib/graphql/resolvers/member.ex index 7f330a975..2af731bcc 100644 --- a/lib/graphql/resolvers/member.ex +++ b/lib/graphql/resolvers/member.ex @@ -121,12 +121,36 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do end end - def remove_member(_parent, %{member_id: member_id, group_id: group_id}, %{ + def update_member(_parent, %{member_id: member_id, role: role}, %{ context: %{current_user: %User{} = user} }) do with %Actor{} = moderator <- Users.get_actor_for_user(user), + %Member{} = member <- Actors.get_member(member_id), + {:ok, _activity, %Member{} = member} <- + ActivityPub.update(member, %{role: role}, true, %{moderator: moderator}) do + {:ok, member} + else + {:has_rights_to_update_role, {:error, :member_not_found}} -> + {:error, "You are not a moderator or admin for this group"} + + {:is_only_admin, true} -> + {:error, + "You can't set yourself to a lower member role for this group because you are the only administrator"} + end + end + + def update_member(_parent, _args, _resolution), + do: {:error, "You must be logged-in to update a member"} + + def remove_member(_parent, %{member_id: member_id, group_id: group_id}, %{ + context: %{current_user: %User{} = user} + }) do + with %Actor{id: moderator_id} = moderator <- Users.get_actor_for_user(user), %Member{} = member <- Actors.get_member(member_id), %Actor{type: :Group} = group <- Actors.get_actor(group_id), + {:has_rights_to_invite, {:ok, %Member{role: role}}} + when role in [:moderator, :administrator, :creator] <- + {:has_rights_to_invite, Actors.get_member(moderator_id, group_id)}, {:ok, _activity, %Member{}} <- ActivityPub.remove(member, group, moderator, true) do {:ok, member} end diff --git a/lib/graphql/schema/actors/member.ex b/lib/graphql/schema/actors/member.ex index 9151b9cb2..d3130cca6 100644 --- a/lib/graphql/schema/actors/member.ex +++ b/lib/graphql/schema/actors/member.ex @@ -72,6 +72,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do resolve(&Member.reject_invitation/3) end + field :update_member, :member do + arg(:member_id, non_null(:id)) + arg(:role, non_null(:member_role_enum)) + + resolve(&Member.update_member/3) + end + @desc "Remove a member from a group" field :remove_member, :member do arg(:group_id, non_null(:id)) diff --git a/lib/web/cache/activity_pub.ex b/lib/web/cache/activity_pub.ex index c4401fd32..bb9df1e41 100644 --- a/lib/web/cache/activity_pub.ex +++ b/lib/web/cache/activity_pub.ex @@ -4,7 +4,7 @@ defmodule Mobilizon.Web.Cache.ActivityPub do """ alias Mobilizon.{Actors, Discussions, Events, Posts, Resources, Todos, Tombstone} - alias Mobilizon.Actors.Actor + alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Events.Event alias Mobilizon.Federation.ActivityPub.Relay @@ -174,6 +174,23 @@ defmodule Mobilizon.Web.Cache.ActivityPub do end) end + @doc """ + Gets a member by its UUID, with all associations loaded. + """ + @spec get_member_by_uuid_with_preload(String.t()) :: + {:commit, Todo.t()} | {:ignore, nil} + def get_member_by_uuid_with_preload(uuid) do + Cachex.fetch(@cache, "member_" <> uuid, fn "member_" <> uuid -> + case Actors.get_member(uuid) do + %Member{} = member -> + {:commit, member} + + nil -> + {:ignore, nil} + end + end) + end + @doc """ Gets a relay. """ diff --git a/lib/web/cache/cache.ex b/lib/web/cache/cache.ex index 9b6bce9e0..5afcd4690 100644 --- a/lib/web/cache/cache.ex +++ b/lib/web/cache/cache.ex @@ -24,6 +24,7 @@ defmodule Mobilizon.Web.Cache do defdelegate get_resource_by_uuid_with_preload(uuid), to: ActivityPub defdelegate get_todo_list_by_uuid_with_preload(uuid), to: ActivityPub defdelegate get_todo_by_uuid_with_preload(uuid), to: ActivityPub + defdelegate get_member_by_uuid_with_preload(uuid), to: ActivityPub defdelegate get_post_by_slug_with_preload(slug), to: ActivityPub defdelegate get_discussion_by_slug_with_preload(slug), to: ActivityPub defdelegate get_relay, to: ActivityPub diff --git a/lib/web/controllers/activity_pub_controller.ex b/lib/web/controllers/activity_pub_controller.ex index e4739a4d9..0e35181c4 100644 --- a/lib/web/controllers/activity_pub_controller.ex +++ b/lib/web/controllers/activity_pub_controller.ex @@ -7,7 +7,7 @@ defmodule Mobilizon.Web.ActivityPubController do use Mobilizon.Web, :controller alias Mobilizon.{Actors, Config} - alias Mobilizon.Actors.Actor + alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.Federator @@ -66,6 +66,41 @@ defmodule Mobilizon.Web.ActivityPubController do actor_collection(conn, "discussions", args) end + @ok_statuses [:ok, :commit] + @spec member(Plug.Conn.t(), map) :: {:error, :not_found} | Plug.Conn.t() + def member(conn, %{"uuid" => uuid}) do + with {status, %Member{parent: %Actor{} = group, actor: %Actor{domain: nil} = _actor} = member} + when status in @ok_statuses <- + Cache.get_member_by_uuid_with_preload(uuid), + actor <- Map.get(conn.assigns, :actor), + true <- actor_applicant_group_member?(group, actor) do + json( + conn, + ActorView.render("member.json", %{ + member: member, + actor_applicant: actor + }) + ) + else + {status, %Member{actor: %Actor{url: domain}, parent: %Actor{} = group, url: url}} + when status in @ok_statuses and not is_nil(domain) -> + with actor <- Map.get(conn.assigns, :actor), + true <- actor_applicant_group_member?(group, actor) do + redirect(conn, external: url) + else + _ -> + conn + |> put_status(404) + |> json("Not found") + end + + _ -> + conn + |> put_status(404) + |> json("Not found") + end + end + def outbox(conn, args) do actor_collection(conn, "outbox", args) end @@ -153,4 +188,15 @@ defmodule Mobilizon.Web.ActivityPubController do ) end end + + defp actor_applicant_group_member?(%Actor{}, nil), do: false + + defp actor_applicant_group_member?(%Actor{id: group_id}, %Actor{id: actor_applicant_id}), + do: + Actors.get_member(actor_applicant_id, group_id, [ + :member, + :moderator, + :administrator, + :creator + ]) != {:error, :member_not_found} end diff --git a/lib/web/router.ex b/lib/web/router.ex index e23ce1eff..df4b06a54 100644 --- a/lib/web/router.ex +++ b/lib/web/router.ex @@ -105,6 +105,7 @@ defmodule Mobilizon.Web.Router do get("/@:name/following", ActivityPubController, :following) get("/@:name/followers", ActivityPubController, :followers) get("/@:name/members", ActivityPubController, :members) + get("/member/:uuid", ActivityPubController, :member) end scope "/", Mobilizon.Web do diff --git a/lib/web/views/activity_pub/actor_view.ex b/lib/web/views/activity_pub/actor_view.ex index 3599cf392..5e536dd7d 100644 --- a/lib/web/views/activity_pub/actor_view.ex +++ b/lib/web/views/activity_pub/actor_view.ex @@ -23,6 +23,12 @@ defmodule Mobilizon.Web.ActivityPub.ActorView do |> Map.merge(Utils.make_json_ld_header()) end + def render("member.json", %{member: %Member{} = member}) do + member + |> Convertible.model_to_as() + |> Map.merge(Utils.make_json_ld_header()) + end + @doc """ Render an actor collection """ diff --git a/test/graphql/resolvers/member_test.exs b/test/graphql/resolvers/member_test.exs index 82cbf020b..9e1fce195 100644 --- a/test/graphql/resolvers/member_test.exs +++ b/test/graphql/resolvers/member_test.exs @@ -447,4 +447,153 @@ defmodule Mobilizon.GraphQL.Resolvers.MemberTest do assert hd(res["errors"])["message"] == "You cannot invite to this group" end end + + describe "Member resolver to update a group member" do + @update_member_mutation """ + mutation UpdateMember($memberId: ID!, $role: MemberRoleEnum!) { + updateMember(memberId: $memberId, role: $role) { + id + role + } + } + """ + + setup %{conn: conn, actor: actor, user: user} do + group = insert(:group) + target_actor = insert(:actor, user: user) + + {:ok, conn: conn, actor: actor, user: user, group: group, target_actor: target_actor} + end + + test "update_member/3 fails when not connected", %{ + conn: conn, + group: group, + target_actor: target_actor + } do + %Member{id: member_id} = + insert(:member, %{actor: target_actor, parent: group, role: :member}) + + res = + conn + |> AbsintheHelpers.graphql_query( + query: @update_member_mutation, + variables: %{ + memberId: member_id, + role: "MODERATOR" + } + ) + + assert hd(res["errors"])["message"] == "You must be logged-in to update a member" + end + + test "update_member/3 fails when not a member of the group", %{ + conn: conn, + group: group, + target_actor: target_actor + } do + user = insert(:user) + actor = insert(:actor, user: user) + Mobilizon.Users.update_user_default_actor(user.id, actor.id) + + %Member{id: member_id} = + insert(:member, %{actor: target_actor, parent: group, role: :member}) + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @update_member_mutation, + variables: %{ + memberId: member_id, + role: "MODERATOR" + } + ) + + assert hd(res["errors"])["message"] == "You are not a moderator or admin for this group" + end + + test "update_member/3 updates the member role", %{ + conn: conn, + user: user, + actor: actor, + group: group, + target_actor: target_actor + } do + Mobilizon.Users.update_user_default_actor(user.id, actor.id) + insert(:member, actor: actor, parent: group, role: :administrator) + + %Member{id: member_id} = + insert(:member, %{actor: target_actor, parent: group, role: :member}) + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @update_member_mutation, + variables: %{ + memberId: member_id, + role: "MODERATOR" + } + ) + + assert is_nil(res["errors"]) + assert res["data"]["updateMember"]["role"] == "MODERATOR" + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @update_member_mutation, + variables: %{ + memberId: member_id, + role: "ADMINISTRATOR" + } + ) + + assert is_nil(res["errors"]) + assert res["data"]["updateMember"]["role"] == "ADMINISTRATOR" + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @update_member_mutation, + variables: %{ + memberId: member_id, + role: "MEMBER" + } + ) + + assert is_nil(res["errors"]) + assert res["data"]["updateMember"]["role"] == "MEMBER" + end + + test "update_member/3 prevents to downgrade the member role if there's no admin left", %{ + conn: conn, + user: user, + actor: actor, + group: group + } do + Mobilizon.Users.update_user_default_actor(user.id, actor.id) + %Member{id: member_id} = insert(:member, actor: actor, parent: group, role: :administrator) + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @update_member_mutation, + variables: %{ + memberId: member_id, + role: "MEMBER" + } + ) + + assert hd(res["errors"])["message"] == + "You can't set yourself to a lower member role for this group because you are the only administrator" + end + end + + describe "Member resolver to remove a member from a group" do + # TODO write tests for me plz + end end diff --git a/test/web/controllers/activity_pub_controller_test.exs b/test/web/controllers/activity_pub_controller_test.exs index e5dcbf921..c5548a52b 100644 --- a/test/web/controllers/activity_pub_controller_test.exs +++ b/test/web/controllers/activity_pub_controller_test.exs @@ -438,4 +438,60 @@ defmodule Mobilizon.Web.ActivityPubControllerTest do assert result["totalItems"] == 2 end end + + describe "/member/:uuid" do + test "it returns a json representation of the member", %{conn: conn} do + group = insert(:group) + remote_actor_2 = insert(:actor, domain: "remote3.tld") + insert(:member, actor: remote_actor_2, parent: group, role: :member) + + member = + insert(:member, + parent: group, + url: "https://someremote.url/member/here" + ) + + conn = + conn + |> assign(:actor, remote_actor_2) + |> put_req_header("accept", "application/activity+json") + |> get(Routes.activity_pub_url(Endpoint, :member, member.id)) + + assert json_response(conn, 200) == + ActorView.render("member.json", %{member: member}) + end + + test "it redirects for remote comments", %{conn: conn} do + group = insert(:group, domain: "remote1.tld") + remote_actor = insert(:actor, domain: "remote2.tld") + remote_actor_2 = insert(:actor, domain: "remote3.tld") + insert(:member, actor: remote_actor_2, parent: group, role: :member) + + member = + insert(:member, + actor: remote_actor, + parent: group, + url: "https://someremote.url/member/here" + ) + + conn = + conn + |> assign(:actor, remote_actor_2) + |> put_req_header("accept", "application/activity+json") + |> get(Routes.activity_pub_url(Endpoint, :member, member.id)) + + assert redirected_to(conn) == "https://someremote.url/member/here" + end + + test "it returns 404 if the fetch is not authenticated", %{conn: conn} do + member = insert(:member) + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get(Routes.activity_pub_url(Endpoint, :member, member.id)) + + assert json_response(conn, 404) + end + end end