From a4deeb55d4a4ce4be52030e12fb3840813e00882 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Fri, 24 Aug 2018 11:34:00 +0200 Subject: [PATCH] More tests --- lib/eventos/activity.ex | 2 +- lib/eventos/actors/actor.ex | 56 +++++-- lib/eventos/actors/actors.ex | 40 +++-- lib/eventos/actors/member.ex | 2 +- lib/eventos/actors/service/reset_password.ex | 20 +-- lib/eventos/events/comment.ex | 12 +- lib/eventos/events/event.ex | 16 +- lib/eventos/events/events.ex | 48 +++++- .../controllers/activity_pub_controller.ex | 8 +- .../controllers/actor_controller.ex | 10 +- .../controllers/group_controller.ex | 18 +-- .../controllers/user_controller.ex | 43 +++--- lib/eventos_web/router.ex | 1 + .../views/activity_pub/actor_view.ex | 11 +- .../views/activity_pub/object_view.ex | 13 ++ lib/service/activity_pub/activity_pub.ex | 132 +++++++++++++---- lib/service/activity_pub/transmogrifier.ex | 23 ++- lib/service/activity_pub/utils.ex | 27 ++-- mix.exs | 1 - ...180816093446_add_primary_key_to_member.exs | 19 +++ test/eventos/actors/actors_test.exs | 125 ++++++++++++++-- test/eventos/events/events_test.exs | 140 +++++++++++------- .../service/activitypub/activitypub_test.exs | 26 +++- .../activity_pub_controller_test.exs | 116 ++++++++++----- .../controllers/actor_controller_test.exs | 58 ++++++++ .../controllers/address_controller_test.exs | 2 +- .../controllers/user_controller_test.exs | 139 ++++++++++++++++- test/fixtures/mastodon-post-activity.json | 65 ++++++++ test/support/factory.ex | 13 +- 29 files changed, 945 insertions(+), 241 deletions(-) create mode 100644 priv/repo/migrations/20180816093446_add_primary_key_to_member.exs create mode 100644 test/fixtures/mastodon-post-activity.json diff --git a/lib/eventos/activity.ex b/lib/eventos/activity.ex index b28d5b615..585fdbce8 100644 --- a/lib/eventos/activity.ex +++ b/lib/eventos/activity.ex @@ -3,5 +3,5 @@ defmodule Eventos.Activity do Represents an activity """ - defstruct [:id, :data, :local, :actor, :recipients, :notifications] + defstruct [:id, :data, :local, :actor, :recipients, :notifications, :type] end diff --git a/lib/eventos/actors/actor.ex b/lib/eventos/actors/actor.ex index 9ee136e54..b3c0381e2 100644 --- a/lib/eventos/actors/actor.ex +++ b/lib/eventos/actors/actor.ex @@ -173,23 +173,9 @@ defmodule Eventos.Actors.Actor do |> put_change(:local, true) end - def get_or_fetch_by_url(url) do - if user = Actors.get_actor_by_url(url) do - user - else - case ActivityPub.make_actor_from_url(url) do - {:ok, user} -> - user - - _ -> - {:error, "Could not fetch by AP id"} - end - end - end - @spec get_public_key_for_url(String.t()) :: {:ok, String.t()} def get_public_key_for_url(url) do - with %Actor{} = actor <- get_or_fetch_by_url(url) do + with {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(url) do actor.keys |> Eventos.Service.ActivityPub.Utils.pem_to_public_key() else @@ -228,4 +214,44 @@ defmodule Eventos.Actors.Actor do ) ) end + + def get_groups_member_of(%Actor{id: actor_id}) do + Repo.all( + from( + a in Actor, + join: m in Member, + on: a.id == m.parent_id, + where: m.actor_id == ^actor_id + ) + ) + end + + def get_members_for_group(%Actor{id: actor_id}) do + Repo.all( + from( + a in Actor, + join: m in Member, + on: a.id == m.actor_id, + where: m.parent_id == ^actor_id + ) + ) + end + + def follow(%Actor{} = follower, %Actor{} = followed) do + # Check if actor is locked + # Check if followed has blocked follower + # Check if follower already follows followed + cond do + following?(follower, followed) -> + {:error, + "Could not follow actor: you are already following #{followed.preferred_username}"} + + # true -> nil + # Follow the person + end + end + + def following?(%Actor{} = follower, %Actor{followers: followers}) do + Enum.member?(followers, follower) + end end diff --git a/lib/eventos/actors/actors.ex b/lib/eventos/actors/actors.ex index 0b0755e3e..77d4042e1 100644 --- a/lib/eventos/actors/actors.ex +++ b/lib/eventos/actors/actors.ex @@ -265,13 +265,11 @@ defmodule Eventos.Actors do def get_or_fetch_by_url(url) do if actor = get_actor_by_url(url) do - actor + {:ok, actor} else - ap_try = ActivityPub.make_actor_from_url(url) - - case ap_try do + case ActivityPub.make_actor_from_url(url) do {:ok, actor} -> - actor + {:ok, actor} _ -> {:error, "Could not fetch by AP id"} @@ -299,7 +297,7 @@ defmodule Eventos.Actors do @doc """ Find actors by their name or displayed name """ - def find_actors_by_username(username) do + def find_actors_by_username_or_name(username) do Repo.all( from( a in Actor, @@ -320,7 +318,7 @@ defmodule Eventos.Actors do @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ def search(name) do # find already saved accounts - case find_actors_by_username(name) do + case find_actors_by_username_or_name(name) do [] -> # no accounts found, let's test if it's an username@domain.tld with true <- Regex.match?(@email_regex, name), @@ -457,6 +455,24 @@ defmodule Eventos.Actors do |> Repo.insert() end + @doc """ + Gets an user by it's email + + ## Examples + + iex> get_user_by_email(user, email) + {:ok, %User{}} + + iex> get_user_by_email(user, wrong_email) + {:error, nil} + """ + def get_user_by_email(email) do + case Repo.get_by(User, email: email) do + nil -> {:error, nil} + user -> {:ok, user} + end + end + @doc """ Updates a user. @@ -548,10 +564,12 @@ defmodule Eventos.Actors do """ def create_member(attrs \\ %{}) do - %Member{} - |> Member.changeset(attrs) - |> Repo.insert!() - |> Repo.preload([:actor, :parent]) + with {:ok, %Member{} = member} <- + %Member{} + |> Member.changeset(attrs) + |> Repo.insert() do + {:ok, Repo.preload(member, [:actor, :parent])} + end end @doc """ diff --git a/lib/eventos/actors/member.ex b/lib/eventos/actors/member.ex index 8e86bd755..9b51991c0 100644 --- a/lib/eventos/actors/member.ex +++ b/lib/eventos/actors/member.ex @@ -7,7 +7,6 @@ defmodule Eventos.Actors.Member do alias Eventos.Actors.Member alias Eventos.Actors.Actor - @primary_key false schema "members" do field(:approved, :boolean, default: true) # 0 : Member, 1 : Moderator, 2 : Admin @@ -23,5 +22,6 @@ defmodule Eventos.Actors.Member do member |> cast(attrs, [:role, :approved, :parent_id, :actor_id]) |> validate_required([:parent_id, :actor_id]) + |> unique_constraint(:parent_id, name: :members_actor_parent_unique_index) end end diff --git a/lib/eventos/actors/service/reset_password.ex b/lib/eventos/actors/service/reset_password.ex index 65b9ad282..8adad04d5 100644 --- a/lib/eventos/actors/service/reset_password.ex +++ b/lib/eventos/actors/service/reset_password.ex @@ -11,16 +11,18 @@ defmodule Eventos.Actors.Service.ResetPassword do """ @spec check_reset_password_token(String.t(), String.t()) :: tuple def check_reset_password_token(password, token) do - with %User{} = user <- Repo.get_by(User, reset_password_token: token) do - Repo.update( - User.password_reset_changeset(user, %{ - "password" => password, - "reset_password_sent_at" => nil, - "reset_password_token" => nil - }) - ) + with %User{} = user <- Repo.get_by(User, reset_password_token: token), + {:ok, %User{} = user} <- + Repo.update( + User.password_reset_changeset(user, %{ + "password" => password, + "reset_password_sent_at" => nil, + "reset_password_token" => nil + }) + ) do + {:ok, Repo.preload(user, :actors)} else - _err -> + err -> {:error, :invalid_token} end end diff --git a/lib/eventos/events/comment.ex b/lib/eventos/events/comment.ex index 1ca5226b6..025604712 100644 --- a/lib/eventos/events/comment.ex +++ b/lib/eventos/events/comment.ex @@ -21,17 +21,23 @@ defmodule Eventos.Events.Comment do belongs_to(:in_reply_to_comment, Comment, foreign_key: :in_reply_to_comment_id) belongs_to(:origin_comment, Comment, foreign_key: :origin_comment_id) - timestamps() + timestamps(type: :utc_datetime) end @doc false def changeset(comment, attrs) do uuid = Ecto.UUID.generate() + # TODO : really change me right away + url = + if Map.has_key?(attrs, "url"), + do: attrs["url"], + else: "#{EventosWeb.Endpoint.url()}/comments/#{uuid}" + comment |> cast(attrs, [:url, :text, :actor_id, :event_id, :in_reply_to_comment_id, :attributed_to_id]) - |> validate_required([:text, :actor_id]) |> put_change(:uuid, uuid) - |> put_change(:url, "#{EventosWeb.Endpoint.url()}/comments/#{uuid}") + |> put_change(:url, url) + |> validate_required([:text, :actor_id, :url]) end end diff --git a/lib/eventos/events/event.ex b/lib/eventos/events/event.ex index 512a58160..dfbfc1569 100644 --- a/lib/eventos/events/event.ex +++ b/lib/eventos/events/event.ex @@ -18,8 +18,11 @@ defmodule Eventos.Events.Event do field(:description, :string) field(:ends_on, Timex.Ecto.DateTimeWithTimezone) field(:title, :string) + # ??? field(:state, :integer, default: 0) + # Event status: TENTATIVE 1, CONFIRMED 2, CANCELLED 3 field(:status, :integer, default: 0) + # If the event is public or private field(:public, :boolean, default: true) field(:thumbnail, :string) field(:large_image, :string) @@ -42,9 +45,7 @@ defmodule Eventos.Events.Event do @doc false def changeset(%Event{} = event, attrs) do - uuid = Ecto.UUID.generate() - - # TODO : check what's the use here. Tests ? + # TODO : Change all of this actor_url = if Map.has_key?(attrs, :organizer_actor) do attrs.organizer_actor.preferred_username @@ -52,6 +53,13 @@ defmodule Eventos.Events.Event do "" end + uuid = Ecto.UUID.generate() + + url = + if Map.has_key?(attrs, "url"), + do: attrs["url"], + else: "#{EventosWeb.Endpoint.url()}/@#{actor_url}/#{uuid}" + event |> Ecto.Changeset.cast(attrs, [ :title, @@ -74,7 +82,7 @@ defmodule Eventos.Events.Event do |> cast_assoc(:tags) |> cast_assoc(:physical_address) |> put_change(:uuid, uuid) - |> put_change(:url, "#{EventosWeb.Endpoint.url()}/@#{actor_url}/#{uuid}") + |> put_change(:url, url) |> validate_required([ :title, :begins_on, diff --git a/lib/eventos/events/events.ex b/lib/eventos/events/events.ex index c6fc748ad..e61e793f4 100644 --- a/lib/eventos/events/events.ex +++ b/lib/eventos/events/events.ex @@ -110,10 +110,17 @@ defmodule Eventos.Events do @doc """ Gets an event by it's URL """ - def get_event_by_url!(url) do + def get_event_by_url(url) do Repo.get_by(Event, url: url) end + @doc """ + Gets an event by it's URL + """ + def get_event_by_url!(url) do + Repo.get_by!(Event, url: url) + end + @doc """ Gets an event by it's UUID """ @@ -175,7 +182,10 @@ defmodule Eventos.Events do @doc """ Find events by name """ + def find_events_by_name(name) when name == "", do: [] + def find_events_by_name(name) do + name = String.trim(name) events = Repo.all(from(a in Event, where: ilike(a.title, ^like_sanitize(name)))) Repo.preload(events, [:organizer_actor]) end @@ -780,6 +790,32 @@ defmodule Eventos.Events do Repo.all(Comment) end + def get_comments_for_actor(%Actor{id: actor_id}, page \\ 1, limit \\ 10) do + start = (page - 1) * limit + + query = + from( + c in Comment, + where: c.actor_id == ^actor_id, + limit: ^limit, + order_by: [desc: :id], + offset: ^start, + preload: [ + :actor, + :in_reply_to_comment, + :origin_comment, + :event + ] + ) + + comments = Repo.all(query) + + count_comments = + Repo.one(from(c in Comment, select: count(c.id), where: c.actor_id == ^actor_id)) + + {:ok, comments, count_comments} + end + @doc """ Gets a single comment. @@ -798,6 +834,16 @@ defmodule Eventos.Events do def get_comment_with_uuid!(uuid), do: Repo.get_by!(Comment, uuid: uuid) + def get_comment_from_url(url), do: Repo.get_by(Comment, url: url) + + def get_comment_from_url!(url), do: Repo.get_by!(Comment, url: url) + + def get_comment_full_from_url!(url) do + with %Comment{} = comment <- Repo.get_by!(Comment, url: url) do + Repo.preload(comment, :actor) + end + end + @doc """ Creates a comment. diff --git a/lib/eventos_web/controllers/activity_pub_controller.ex b/lib/eventos_web/controllers/activity_pub_controller.ex index 34ee75378..3789d9c4a 100644 --- a/lib/eventos_web/controllers/activity_pub_controller.ex +++ b/lib/eventos_web/controllers/activity_pub_controller.ex @@ -18,10 +18,16 @@ defmodule EventosWeb.ActivityPubController do end def event(conn, %{"uuid" => uuid}) do - with %Event{} = event <- Events.get_event_full_by_uuid(uuid) do + with %Event{} = event <- Events.get_event_full_by_uuid(uuid), + true <- event.public do conn |> put_resp_header("content-type", "application/activity+json") |> json(ObjectView.render("event.json", %{event: event})) + else + false -> + conn + |> put_status(404) + |> json("Not found") end end diff --git a/lib/eventos_web/controllers/actor_controller.ex b/lib/eventos_web/controllers/actor_controller.ex index 18dc166dc..c3ea9fbee 100644 --- a/lib/eventos_web/controllers/actor_controller.ex +++ b/lib/eventos_web/controllers/actor_controller.ex @@ -37,8 +37,12 @@ defmodule EventosWeb.ActorController do end def show(conn, %{"name" => name}) do - actor = Actors.get_actor_by_name_with_everything(name) - render(conn, "show.json", actor: actor) + with %Actor{} = actor <- Actors.get_actor_by_name_with_everything(name) do + render(conn, "show.json", actor: actor) + else + nil -> + send_resp(conn, :not_found, "") + end end @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ @@ -57,7 +61,7 @@ defmodule EventosWeb.ActorController do actor = Actors.get_local_actor_by_name(name) with {:ok, %Actor{} = actor} <- Actors.update_actor(actor, actor_params) do - render(conn, "show.json", actor: actor) + render(conn, "show_basic.json", actor: actor) end end diff --git a/lib/eventos_web/controllers/group_controller.ex b/lib/eventos_web/controllers/group_controller.ex index f55df79b7..5808b9ec4 100644 --- a/lib/eventos_web/controllers/group_controller.ex +++ b/lib/eventos_web/controllers/group_controller.ex @@ -15,15 +15,13 @@ defmodule EventosWeb.GroupController do end def create(conn, %{"group" => group_params}) do - with {:ok, %Actor{} = group} <- Actors.create_group(group_params) do - %Member{} = - _member = - Actors.create_member(%{ - "parent_id" => group.id, - "actor_id" => Actors.get_local_actor_by_name(group_params["actor_admin"]).id, - "role" => 2 - }) - + with {:ok, %Actor{} = group} <- Actors.create_group(group_params), + {:ok, %Member{} = member} <- + Actors.create_member(%{ + "parent_id" => group.id, + "actor_id" => Actors.get_local_actor_by_name(group_params["actor_admin"]).id, + "role" => 2 + }) do conn |> put_status(:created) |> put_resp_header("location", actor_path(conn, :show, group)) @@ -34,7 +32,7 @@ defmodule EventosWeb.GroupController do def join(conn, %{"name" => group_name, "actor_name" => actor_name}) do with %Actor{} = group <- Actors.get_group_by_name(group_name), %Actor{} = actor <- Actors.get_local_actor_by_name(actor_name), - %Member{} = member <- + {:ok, %Member{} = member} <- Actors.create_member(%{"parent_id" => group.id, "actor_id" => actor.id}) do conn |> put_status(:created) diff --git a/lib/eventos_web/controllers/user_controller.ex b/lib/eventos_web/controllers/user_controller.ex index 6ce2695a1..7bb50d1bc 100644 --- a/lib/eventos_web/controllers/user_controller.ex +++ b/lib/eventos_web/controllers/user_controller.ex @@ -42,11 +42,15 @@ defmodule EventosWeb.UserController do end end + @time_before_resend 3600 def resend_confirmation(conn, %{"email" => email}) do with {:ok, %User{} = user} <- Actors.find_by_email(email), false <- is_nil(user.confirmation_token), true <- - Timex.before?(Timex.shift(user.confirmation_sent_at, hours: 1), DateTime.utc_now()) do + Timex.before?( + Timex.shift(user.confirmation_sent_at, seconds: @time_before_resend), + DateTime.utc_now() + ) do Activation.resend_confirmation_email(user) render(conn, "confirmation.json", %{user: user}) else @@ -58,7 +62,10 @@ defmodule EventosWeb.UserController do _ -> conn |> put_status(:not_found) - |> json(%{"error" => "Unable to resend the validation token"}) + |> json(%{ + "error" => + "Unable to resend the validation token. Please wait a while before you can ask for resending token" + }) end end @@ -67,7 +74,7 @@ defmodule EventosWeb.UserController do {:ok, _} <- ResetPassword.send_password_reset_email(user) do render(conn, "password_reset.json", %{user: user}) else - {:error, :not_found} -> + {:error, nil} -> conn |> put_status(:not_found) |> json(%{"errors" => "Unable to find an user with this email"}) @@ -105,23 +112,23 @@ defmodule EventosWeb.UserController do render(conn, "show_simple.json", user: user) end - defp handle_changeset_errors(errors) do - errors - |> Enum.map(fn {field, detail} -> - "#{field} " <> render_detail(detail) - end) - |> Enum.join() - end + # defp handle_changeset_errors(errors) do + # errors + # |> Enum.map(fn {field, detail} -> + # "#{field} " <> render_detail(detail) + # end) + # |> Enum.join() + # end - defp render_detail({message, values}) do - Enum.reduce(values, message, fn {k, v}, acc -> - String.replace(acc, "%{#{k}}", to_string(v)) - end) - end + # defp render_detail({message, values}) do + # Enum.reduce(values, message, fn {k, v}, acc -> + # String.replace(acc, "%{#{k}}", to_string(v)) + # end) + # end - defp render_detail(message) do - message - end + # defp render_detail(message) do + # message + # end def update(conn, %{"id" => id, "user" => user_params}) do user = Actors.get_user!(id) diff --git a/lib/eventos_web/router.ex b/lib/eventos_web/router.ex index d35888192..75a8a36a2 100644 --- a/lib/eventos_web/router.ex +++ b/lib/eventos_web/router.ex @@ -122,6 +122,7 @@ defmodule EventosWeb.Router do get("/@:name/following", ActivityPubController, :following) get("/@:name/followers", ActivityPubController, :followers) get("/events/:uuid", ActivityPubController, :event) + get("/comments/:uuid", ActivityPubController, :event) post("/@:name/inbox", ActivityPubController, :inbox) post("/inbox", ActivityPubController, :inbox) end diff --git a/lib/eventos_web/views/activity_pub/actor_view.ex b/lib/eventos_web/views/activity_pub/actor_view.ex index cab69b96e..3742be4f1 100644 --- a/lib/eventos_web/views/activity_pub/actor_view.ex +++ b/lib/eventos_web/views/activity_pub/actor_view.ex @@ -134,10 +134,17 @@ defmodule EventosWeb.ActivityPub.ActorView do else "Announce" end, - "actor" => activity.data.organizer_actor.url, + "actor" => activity.actor, "published" => Timex.now(), "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "object" => render_one(activity.data, ObjectView, "event.json", as: :event) + "object" => + case activity.type do + :Event -> + render_one(activity.data, ObjectView, "event.json", as: :event) + + :Comment -> + render_one(activity.data, ObjectView, "note.json", as: :note) + end } end diff --git a/lib/eventos_web/views/activity_pub/object_view.ex b/lib/eventos_web/views/activity_pub/object_view.ex index 4a03510bc..90e2ed560 100644 --- a/lib/eventos_web/views/activity_pub/object_view.ex +++ b/lib/eventos_web/views/activity_pub/object_view.ex @@ -32,6 +32,19 @@ defmodule EventosWeb.ActivityPub.ObjectView do Map.merge(event, @base) end + def render("note.json", %{note: note}) do + event = %{ + "type" => "Note", + "id" => note.url, + "content" => note.text, + "mediaType" => "text/markdown", + "published" => Timex.format!(note.inserted_at, "{ISO:Extended}"), + "updated" => Timex.format!(note.updated_at, "{ISO:Extended}") + } + + Map.merge(event, @base) + end + def render("category.json", %{category: category}) do %{"title" => category.title} end diff --git a/lib/service/activity_pub/activity_pub.ex b/lib/service/activity_pub/activity_pub.ex index 1eaa3576b..b40d4a97f 100644 --- a/lib/service/activity_pub/activity_pub.ex +++ b/lib/service/activity_pub/activity_pub.ex @@ -6,7 +6,7 @@ defmodule Eventos.Service.ActivityPub do """ alias Eventos.Events - alias Eventos.Events.{Event, Category} + alias Eventos.Events.{Event, Category, Comment} alias Eventos.Service.ActivityPub.Transmogrifier alias Eventos.Service.WebFinger alias Eventos.Activity @@ -44,36 +44,65 @@ defmodule Eventos.Service.ActivityPub do end end - def fetch_event_from_url(url) do - if object = Events.get_event_by_url!(url) do - {:ok, object} + def fetch_object_from_url(url, :event), do: fetch_event_from_url(url) + def fetch_object_from_url(url, :note), do: fetch_note_from_url(url) + + @spec fetch_object_from_url(String.t()) :: tuple() + def fetch_object_from_url(url) do + with true <- String.starts_with?(url, "http"), + {:ok, %{body: body, status_code: code}} when code in 200..299 <- + HTTPoison.get( + url, + [Accept: "application/activity+json"], + follow_redirect: true, + timeout: 10_000, + recv_timeout: 20_000 + ), + {:ok, data} <- Jason.decode(body), + nil <- Events.get_event_by_url(data["id"]), + nil <- Events.get_comment_from_url(data["id"]), + params <- %{ + "type" => "Create", + "to" => data["to"], + "cc" => data["cc"], + "actor" => data["attributedTo"], + "object" => data + }, + {:ok, activity} <- Transmogrifier.handle_incoming(params) do + case data["type"] do + "Event" -> + {:ok, Events.get_event_by_url!(activity.data["object"]["id"])} + + "Note" -> + {:ok, Events.get_comment_from_url!(activity.data["object"]["id"])} + end else + object = %Event{} -> {:ok, object} + object = %Comment{} -> {:ok, object} + e -> {:error, e} + end + end + + @spec fetch_object_from_url(String.t()) :: tuple() + def fetch_event_from_url(url) do + with nil <- Events.get_event_by_url(url) do + Logger.info("Fetching #{url} via AP") + fetch_object_from_url(url) + else + %Event{} = comment -> + {:ok, comment} + end + end + + @spec fetch_object_from_url(String.t()) :: tuple() + def fetch_note_from_url(url) do + with nil <- Events.get_comment_from_url(url) do Logger.info("Fetching #{url} via AP") - with true <- String.starts_with?(url, "http"), - {:ok, %{body: body, status_code: code}} when code in 200..299 <- - HTTPoison.get( - url, - [Accept: "application/activity+json"], - follow_redirect: true, - timeout: 10_000, - recv_timeout: 20_000 - ), - {:ok, data} <- Jason.decode(body), - nil <- Events.get_event_by_url!(data["id"]), - params <- %{ - "type" => "Create", - "to" => data["to"], - "cc" => data["cc"], - "actor" => data["attributedTo"], - "object" => data - }, - {:ok, activity} <- Transmogrifier.handle_incoming(params) do - {:ok, Events.get_event_by_url!(activity.data["object"]["id"])} - else - object = %Event{} -> {:ok, object} - e -> e - end + fetch_object_from_url(url) + else + %Comment{} = comment -> + {:ok, comment} end end @@ -127,7 +156,7 @@ defmodule Eventos.Service.ActivityPub do end end - def follow(follower, followed, activity_id \\ nil, local \\ true) do + def follow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do with data <- make_follow_data(follower, followed, activity_id), {:ok, activity} <- insert(data, local), :ok <- maybe_federate(activity) do @@ -135,7 +164,9 @@ defmodule Eventos.Service.ActivityPub do end end - def delete(%Event{url: url, organizer_actor: actor} = event, local \\ true) do + def delete(object, local \\ true) + + def delete(%Event{url: url, organizer_actor: actor} = event, local) do data = %{ "type" => "Delete", "actor" => actor.url, @@ -150,6 +181,21 @@ defmodule Eventos.Service.ActivityPub do end end + def delete(%Comment{url: url, actor: actor} = comment, local) do + data = %{ + "type" => "Delete", + "actor" => actor.url, + "object" => url, + "to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"] + } + + with Events.delete_comment(comment), + {:ok, activity} <- insert(data, local), + :ok <- maybe_federate(activity) do + {:ok, activity} + end + end + def create_public_activities(%Actor{} = actor) do end @@ -285,13 +331,22 @@ defmodule Eventos.Service.ActivityPub do case actor.type do :Person -> {:ok, events, total} = Events.get_events_for_actor(actor, page, limit) + {:ok, comments, total} = Events.get_comments_for_actor(actor, page, limit) - activities = + event_activities = Enum.map(events, fn event -> {:ok, activity} = event_to_activity(event) activity end) + comment_activities = + Enum.map(comments, fn comment -> + {:ok, activity} = comment_to_activity(comment) + activity + end) + + activities = event_activities ++ comment_activities + {activities, total} :Service -> @@ -322,6 +377,7 @@ defmodule Eventos.Service.ActivityPub do defp event_to_activity(%Event{} = event, local \\ true) do activity = %Activity{ + type: :Event, data: event, local: local, actor: event.organizer_actor.url, @@ -333,6 +389,20 @@ defmodule Eventos.Service.ActivityPub do {:ok, activity} end + defp comment_to_activity(%Comment{} = comment, local \\ true) do + activity = %Activity{ + type: :Comment, + data: comment, + local: local, + actor: comment.actor.url, + recipients: ["https://www.w3.org/ns/activitystreams#Public"] + } + + # Notification.create_notifications(activity) + # stream_out(activity) + {:ok, activity} + end + defp ical_event_to_activity(%ExIcal.Event{} = ical_event, %Actor{} = actor, source) do # Logger.debug(inspect ical_event) # TODO : refactor me ! diff --git a/lib/service/activity_pub/transmogrifier.ex b/lib/service/activity_pub/transmogrifier.ex index d4a848900..65f6607bb 100644 --- a/lib/service/activity_pub/transmogrifier.ex +++ b/lib/service/activity_pub/transmogrifier.ex @@ -4,7 +4,7 @@ defmodule Eventos.Service.ActivityPub.Transmogrifier do """ alias Eventos.Actors.Actor alias Eventos.Actors - alias Eventos.Events.Event + alias Eventos.Events.{Event, Comment} alias Eventos.Service.ActivityPub import Ecto.Query @@ -77,9 +77,9 @@ defmodule Eventos.Service.ActivityPub.Transmogrifier do # - tags # - emoji def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do - Logger.debug("Handle incoming to create notes") + Logger.info("Handle incoming to create notes") - with %Actor{} = actor <- Actor.get_or_fetch_by_url(data["actor"]) do + with {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(data["actor"]) do Logger.debug("found actor") object = fix_object(data["object"]) @@ -104,8 +104,8 @@ defmodule Eventos.Service.ActivityPub.Transmogrifier do def handle_incoming( %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data ) do - with %Actor{} = followed <- Actors.get_actor_by_url(followed), - %Actor{} = follower <- Actors.get_or_fetch_by_url(follower), + with {:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed), + {:ok, %Actor{} = follower} <- Actors.get_or_fetch_by_url(follower), {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do ActivityPub.accept(%{to: [follower.url], actor: followed.url, object: data, local: true}) @@ -133,7 +133,7 @@ defmodule Eventos.Service.ActivityPub.Transmogrifier do def handle_incoming( %{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data ) do - with %Actor{} = actor <- Actors.get_or_fetch_by_url(actor), + with {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor), {:ok, object} <- get_obj_helper(object_id) || ActivityPub.fetch_event_from_url(object_id), {:ok, activity, object} <- ActivityPub.announce(actor, object, id, false) do @@ -268,6 +268,17 @@ defmodule Eventos.Service.ActivityPub.Transmogrifier do {:ok, event} end + def prepare_outgoing(%Comment{} = comment) do + comment = + comment + |> Map.from_struct() + |> Map.drop([:__meta__]) + |> Map.put(:"@context", "https://www.w3.org/ns/activitystreams") + |> prepare_object + + {:ok, comment} + end + # # def maybe_fix_object_url(data) do # if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do diff --git a/lib/service/activity_pub/utils.ex b/lib/service/activity_pub/utils.ex index 5aea7f79c..9a59abc4f 100644 --- a/lib/service/activity_pub/utils.ex +++ b/lib/service/activity_pub/utils.ex @@ -132,20 +132,23 @@ defmodule Eventos.Service.ActivityPub.Utils do """ def insert_full_object(%{"object" => %{"type" => type} = object_data}) when is_map(object_data) and type == "Note" do - import Logger - Logger.debug("insert full object") - Logger.debug(inspect(object_data)) - actor = Actors.get_actor_by_url(object_data["actor"]) + with {:ok, %Actor{id: actor_id}} <- Actors.get_or_fetch_by_url(object_data["actor"]) do + data = %{ + "text" => object_data["content"], + "url" => object_data["id"], + "actor_id" => actor_id, + "in_reply_to_comment_id" => object_data["inReplyTo"] + } - data = %{ - "text" => object_data["content"], - "url" => object_data["id"], - "actor_id" => actor.id, - "in_reply_to_comment_id" => object_data["inReplyTo"] - } + require Logger + Logger.info("comment data ready to be inserted") + Logger.info(inspect(data)) - with {:ok, _} <- Events.create_comment(data) do - :ok + with {:ok, comm} <- Events.create_comment(data) do + Logger.info("comment inserted") + Logger.info(inspect(comm)) + :ok + end end end diff --git a/mix.exs b/mix.exs index f81a25f92..ffe09a223 100644 --- a/mix.exs +++ b/mix.exs @@ -65,7 +65,6 @@ defmodule Eventos.Mixfile do {:timex_ecto, "~> 3.0"}, {:icalendar, "~> 0.6"}, {:exgravatar, "~> 2.0.1"}, - {:littlefinger, "~> 0.1"}, {:httpoison, "~> 1.0"}, {:json_ld, "~> 0.2"}, {:jason, "~> 1.0"}, diff --git a/priv/repo/migrations/20180816093446_add_primary_key_to_member.exs b/priv/repo/migrations/20180816093446_add_primary_key_to_member.exs new file mode 100644 index 000000000..57a5b6853 --- /dev/null +++ b/priv/repo/migrations/20180816093446_add_primary_key_to_member.exs @@ -0,0 +1,19 @@ +defmodule Eventos.Repo.Migrations.AddPrimaryKeyToMember do + use Ecto.Migration + + def up do + execute("ALTER TABLE members DROP CONSTRAINT IF EXISTS members_pkey") + drop_if_exists index(:members, ["members_account_id_index"]) + create unique_index(:members, [:actor_id, :parent_id], name: :members_actor_parent_unique_index) + alter table(:members) do + add :id, :serial, primary_key: true + end + end + + def down do + drop index(:members, [:actor_id, :parent_id], name: :members_actor_parent_unique_index) + alter table(:members) do + remove :id + end + end +end diff --git a/test/eventos/actors/actors_test.exs b/test/eventos/actors/actors_test.exs index c7f972554..a3522fd5d 100644 --- a/test/eventos/actors/actors_test.exs +++ b/test/eventos/actors/actors_test.exs @@ -9,7 +9,7 @@ defmodule Eventos.ActorsTest do @valid_attrs %{ summary: "some description", - name: "some name", + name: "Bobby Blank", domain: "some domain", keys: "some keypair", suspended: true, @@ -74,7 +74,7 @@ defmodule Eventos.ActorsTest do end test "get_actor_by_name/1 returns a remote actor" do - assert %Actor{} = actor = Actors.get_or_fetch_by_url(@remote_account_url) + assert {:ok, %Actor{} = actor} = Actors.get_or_fetch_by_url(@remote_account_url) actor_found = Actors.get_actor_by_name("#{actor.preferred_username}@#{actor.domain}") assert actor_found = actor end @@ -107,7 +107,7 @@ defmodule Eventos.ActorsTest do end test "get_actor_by_name_with_everything!/1 returns the remote actor with it's organized events" do - assert %Actor{} = actor = Actors.get_or_fetch_by_url(@remote_account_url) + assert {:ok, %Actor{} = actor} = Actors.get_or_fetch_by_url(@remote_account_url) assert Actors.get_actor_by_name_with_everything( "#{actor.preferred_username}@#{actor.domain}" @@ -124,12 +124,15 @@ defmodule Eventos.ActorsTest do test "get_or_fetch_by_url/1 returns the local actor for the url", %{ actor: actor } do - assert Actors.get_or_fetch_by_url(actor.url).preferred_username == actor.preferred_username - assert Actors.get_or_fetch_by_url(actor.url).domain == nil + preferred_username = actor.preferred_username + + assert {:ok, %Actor{preferred_username: preferred_username, domain: nil} = actor_found} = + Actors.get_or_fetch_by_url(actor.url) end test "get_or_fetch_by_url/1 returns the remote actor for the url" do - assert %Actor{preferred_username: @remote_account_username, domain: @remote_account_domain} = + assert {:ok, + %Actor{preferred_username: @remote_account_username, domain: @remote_account_domain}} = Actors.get_or_fetch_by_url(@remote_account_url) end @@ -141,12 +144,21 @@ defmodule Eventos.ActorsTest do assert actors = [actor, actor2] end - test "test find_actors_by_username/1 returns actors with similar usernames", %{actor: actor} do - %Actor{} = actor2 = Actors.get_or_fetch_by_url(@remote_account_url) - actors = Actors.find_actors_by_username("t") + test "test find_actors_by_username_or_name/1 returns actors with similar usernames", %{ + actor: actor + } do + {:ok, %Actor{} = actor2} = Actors.get_or_fetch_by_url(@remote_account_url) + actors = Actors.find_actors_by_username_or_name("t") assert actors = [actor, actor2] end + test "test find_actors_by_username_or_name/1 returns actors with similar names", %{ + actor: actor + } do + actors = Actors.find_actors_by_username_or_name("ohno") + assert actors == [] + end + test "test search/1 returns accounts for search with existing accounts", %{actor: actor} do assert {:ok, [actor]} = Actors.search("t") end @@ -180,7 +192,7 @@ defmodule Eventos.ActorsTest do test "create_actor/1 with valid data creates a actor" do assert {:ok, %Actor{} = actor} = Actors.create_actor(@valid_attrs) assert actor.summary == "some description" - assert actor.name == "some name" + assert actor.name == "Bobby Blank" assert actor.domain == "some domain" assert actor.keys == "some keypair" assert actor.suspended @@ -484,4 +496,97 @@ defmodule Eventos.ActorsTest do assert %Ecto.Changeset{} = Actors.change_follower(follower) end end + + describe "members" do + alias Eventos.Actors.Member + alias Eventos.Actors.Actor + + @valid_attrs %{approved: true, role: 0} + @update_attrs %{approved: false, role: 1} + @invalid_attrs %{approved: nil, role: nil} + + setup do + actor = insert(:actor) + group = insert(:group) + {:ok, actor: actor, group: group} + end + + defp create_member(%{actor: actor, group: group}) do + insert(:member, actor: actor, parent: group) + end + + test "get_member!/1 returns the member with given id", context do + member = create_member(context) + assert member = Actors.get_member!(member.id) + end + + test "create_member/1 with valid data creates a member", %{ + actor: actor, + group: group + } do + valid_attrs = + @valid_attrs + |> Map.put(:actor_id, actor.id) + |> Map.put(:parent_id, group.id) + + assert {:ok, %Member{} = member} = Actors.create_member(valid_attrs) + assert member.approved == true + assert member.role == 0 + + assert [group] = Actor.get_groups_member_of(actor) + assert [actor] = Actor.get_members_for_group(group) + end + + test "create_member/1 with valid data but same actors fails to create a member", %{ + actor: actor, + group: group + } do + create_member(%{actor: actor, group: group}) + + valid_attrs = + @valid_attrs + |> Map.put(:actor_id, actor.id) + |> Map.put(:parent_id, group.id) + + assert {:error, _member} = Actors.create_member(valid_attrs) + end + + test "create_member/1 with invalid data returns error changeset", %{ + actor: actor, + group: group + } do + invalid_attrs = + @invalid_attrs + |> Map.put(:actor_id, nil) + |> Map.put(:parent_id, nil) + + assert {:error, %Ecto.Changeset{}} = Actors.create_member(invalid_attrs) + end + + test "update_member/2 with valid data updates the member", context do + member = create_member(context) + assert {:ok, member} = Actors.update_member(member, @update_attrs) + assert %Member{} = member + assert member.approved == false + assert member.role == 1 + end + + # This can't happen, since attrs are optional + # test "update_member/2 with invalid data returns error changeset", context do + # member = create_member(context) + # assert {:error, %Ecto.Changeset{}} = Actors.update_member(member, @invalid_attrs) + # assert member = Actors.get_member!(member.id) + # end + + test "delete_member/1 deletes the member", context do + member = create_member(context) + assert {:ok, %Member{}} = Actors.delete_member(member) + assert_raise Ecto.NoResultsError, fn -> Actors.get_member!(member.id) end + end + + test "change_member/1 returns a member changeset", context do + member = create_member(context) + assert %Ecto.Changeset{} = Actors.change_member(member) + end + end end diff --git a/test/eventos/events/events_test.exs b/test/eventos/events/events_test.exs index b455e1b19..2623509ad 100644 --- a/test/eventos/events/events_test.exs +++ b/test/eventos/events/events_test.exs @@ -32,6 +32,12 @@ defmodule Eventos.EventsTest do describe "events" do alias Eventos.Events.Event + setup do + actor = insert(:actor) + event = insert(:event, organizer_actor: actor) + {:ok, actor: actor, event: event} + end + @valid_attrs %{ begins_on: "2010-04-17 14:00:00.000000Z", description: "some description", @@ -46,14 +52,32 @@ defmodule Eventos.EventsTest do } @invalid_attrs %{begins_on: nil, description: nil, ends_on: nil, title: nil} - test "list_events/0 returns all events" do - event = event_fixture() - assert hd(Events.list_events()).title == event.title + test "list_events/0 returns all events", %{event: event} do + assert event.title == hd(Events.list_events()).title end - test "get_event!/1 returns the event with given id" do - event = event_fixture() + test "get_event!/1 returns the event with given id", %{event: event} do assert Events.get_event!(event.id).title == event.title + refute Ecto.assoc_loaded?(Events.get_event!(event.id).organizer_actor) + end + + test "get_event_full!/1 returns the event with given id", %{event: event} do + assert Events.get_event_full!(event.id).organizer_actor.preferred_username == + event.organizer_actor.preferred_username + + assert Events.get_event_full!(event.id).participants == [] + end + + test "find_events_by_name/1 returns events for a given name", %{event: event} do + assert event.title == hd(Events.find_events_by_name(event.title)).title + + event2 = insert(:event, title: "Special event") + assert event2.title == hd(Events.find_events_by_name("Special")).title + + event2 = insert(:event, title: "Special event") + assert event2.title == hd(Events.find_events_by_name(" Special ")).title + + assert [] == Events.find_events_by_name("") end test "create_event/1 with valid data creates a event" do @@ -79,8 +103,7 @@ defmodule Eventos.EventsTest do assert {:error, %Ecto.Changeset{}} = Events.create_event(@invalid_attrs) end - test "update_event/2 with valid data updates the event" do - event = event_fixture() + test "update_event/2 with valid data updates the event", %{event: event} do assert {:ok, event} = Events.update_event(event, @update_attrs) assert %Event{} = event assert event.begins_on == DateTime.from_naive!(~N[2011-05-18 15:01:01.000000Z], "Etc/UTC") @@ -89,27 +112,44 @@ defmodule Eventos.EventsTest do assert event.title == "some updated title" end - test "update_event/2 with invalid data returns error changeset" do - event = event_fixture() + test "update_event/2 with invalid data returns error changeset", %{event: event} do assert {:error, %Ecto.Changeset{}} = Events.update_event(event, @invalid_attrs) assert event.title == Events.get_event!(event.id).title end - test "delete_event/1 deletes the event" do - event = event_fixture() + test "delete_event/1 deletes the event", %{event: event} do assert {:ok, %Event{}} = Events.delete_event(event) assert_raise Ecto.NoResultsError, fn -> Events.get_event!(event.id) end end - test "change_event/1 returns a event changeset" do - event = event_fixture() + test "change_event/1 returns a event changeset", %{event: event} do assert %Ecto.Changeset{} = Events.change_event(event) end + + test "get_events_for_actor/1", %{actor: actor, event: event} do + assert {:ok, [event_found], 1} = Events.get_events_for_actor(actor) + assert event_found.title == event.title + end + + test "get_events_for_actor/3", %{actor: actor, event: event} do + event1 = insert(:event, organizer_actor: actor) + assert {:ok, [event_found, event1_found], 2} = Events.get_events_for_actor(actor, 1, 10) + end + + test "get_events_for_actor/3 with limited results", %{actor: actor, event: event} do + event1 = insert(:event, organizer_actor: actor) + assert {:ok, [event_found], 2} = Events.get_events_for_actor(actor, 1, 1) + end end describe "categories" do alias Eventos.Events.Category + setup do + category = insert(:category) + {:ok, category: category} + end + @valid_attrs %{description: "some description", picture: "some picture", title: "some title"} @update_attrs %{ description: "some updated description", @@ -118,16 +158,18 @@ defmodule Eventos.EventsTest do } @invalid_attrs %{description: nil, picture: nil, title: nil} - test "list_categories/0 returns all categories" do - category = category_fixture() + test "list_categories/0 returns all categories", %{category: category} do assert Events.list_categories() == [category] end - test "get_category!/1 returns the category with given id" do - category = category_fixture() + test "get_category!/1 returns the category with given id", %{category: category} do assert Events.get_category!(category.id) == category end + test "get_category_by_title/1 return the category with given title", %{category: category} do + assert Events.get_category_by_title(category.title) == category + end + test "create_category/1 with valid data creates a category" do assert {:ok, %Category{} = category} = Events.create_category(@valid_attrs) assert category.description == "some description" @@ -139,8 +181,7 @@ defmodule Eventos.EventsTest do assert {:error, %Ecto.Changeset{}} = Events.create_category(@invalid_attrs) end - test "update_category/2 with valid data updates the category" do - category = category_fixture() + test "update_category/2 with valid data updates the category", %{category: category} do assert {:ok, category} = Events.update_category(category, @update_attrs) assert %Category{} = category assert category.description == "some updated description" @@ -148,20 +189,17 @@ defmodule Eventos.EventsTest do assert category.title == "some updated title" end - test "update_category/2 with invalid data returns error changeset" do - category = category_fixture() + test "update_category/2 with invalid data returns error changeset", %{category: category} do assert {:error, %Ecto.Changeset{}} = Events.update_category(category, @invalid_attrs) assert category == Events.get_category!(category.id) end - test "delete_category/1 deletes the category" do - category = category_fixture() + test "delete_category/1 deletes the category", %{category: category} do assert {:ok, %Category{}} = Events.delete_category(category) assert_raise Ecto.NoResultsError, fn -> Events.get_category!(category.id) end end - test "change_category/1 returns a category changeset" do - category = category_fixture() + test "change_category/1 returns a category changeset", %{category: category} do assert %Ecto.Changeset{} = Events.change_category(category) end end @@ -227,35 +265,31 @@ defmodule Eventos.EventsTest do end describe "participants" do - alias Eventos.Events.Participant + alias Eventos.Events.{Participant, Event} + alias Eventos.Actors.Actor @valid_attrs %{role: 42} @update_attrs %{role: 43} @invalid_attrs %{role: nil} - def participant_fixture(attrs \\ %{}) do - event = event_fixture() - actor = actor_fixture() - valid_attrs = Map.put(@valid_attrs, :event_id, event.id) - valid_attrs = Map.put(valid_attrs, :actor_id, actor.id) - - {:ok, participant} = - attrs - |> Enum.into(valid_attrs) - |> Events.create_participant() - - participant + setup do + actor = insert(:actor) + event = insert(:event, organizer_actor: actor) + participant = insert(:participant, actor: actor, event: event) + {:ok, participant: participant, event: event, actor: actor} end - test "list_participants/0 returns all participants" do - participant = participant_fixture() - assert Events.list_participants() == [participant] + test "list_participants/0 returns all participants", %{participant: participant} do + assert [%Participant{} = participant] = Events.list_participants() end - # test "get_participant!/1 returns the participant with given id" do - # participant = participant_fixture() - # assert Events.get_participant!(participant.id) == participant - # end + test "get_participant!/1 returns the participant for a given event and given actor", %{ + event: %Event{id: event_id} = _event, + actor: %Actor{id: actor_id} = _actor + } do + assert %Participant{event_id: event_id, actor_id: actor_id} = + _participant = Events.get_participant!(event_id, actor_id) + end test "create_participant/1 with valid data creates a participant" do actor = actor_fixture() @@ -270,25 +304,25 @@ defmodule Eventos.EventsTest do assert {:error, %Ecto.Changeset{}} = Events.create_participant(@invalid_attrs) end - test "update_participant/2 with valid data updates the participant" do - participant = participant_fixture() + test "update_participant/2 with valid data updates the participant", %{ + participant: participant + } do assert {:ok, participant} = Events.update_participant(participant, @update_attrs) assert %Participant{} = participant assert participant.role == 43 end - test "update_participant/2 with invalid data returns error changeset" do - participant = participant_fixture() + test "update_participant/2 with invalid data returns error changeset", %{ + participant: participant + } do assert {:error, %Ecto.Changeset{}} = Events.update_participant(participant, @invalid_attrs) end - test "delete_participant/1 deletes the participant" do - participant = participant_fixture() + test "delete_participant/1 deletes the participant", %{participant: participant} do assert {:ok, %Participant{}} = Events.delete_participant(participant) end - test "change_participant/1 returns a participant changeset" do - participant = participant_fixture() + test "change_participant/1 returns a participant changeset", %{participant: participant} do assert %Ecto.Changeset{} = Events.change_participant(participant) end end diff --git a/test/eventos/service/activitypub/activitypub_test.exs b/test/eventos/service/activitypub/activitypub_test.exs index 300606001..3cf40fbef 100644 --- a/test/eventos/service/activitypub/activitypub_test.exs +++ b/test/eventos/service/activitypub/activitypub_test.exs @@ -5,14 +5,20 @@ defmodule Eventos.Service.Activitypub.ActivitypubTest do alias Eventos.Events alias Eventos.Actors.Actor + alias Eventos.Actors alias Eventos.Service.ActivityPub alias Eventos.Activity describe "fetching actor from it's url" do - test "returns an actor" do + test "returns an actor from nickname" do assert {:ok, %Actor{preferred_username: "tcit", domain: "framapiaf.org"} = actor} = ActivityPub.make_actor_from_nickname("tcit@framapiaf.org") end + + test "returns an actor from url" do + assert {:ok, %Actor{preferred_username: "tcit", domain: "framapiaf.org"}} = + Actors.get_or_fetch_by_url("https://framapiaf.org/users/tcit") + end end describe "create activities" do @@ -33,8 +39,8 @@ defmodule Eventos.Service.Activitypub.ActivitypubTest do end end - describe "fetching an object" do - test "it fetches an object" do + describe "fetching an" do + test "event by url" do {:ok, object} = ActivityPub.fetch_event_from_url("https://social.tcit.fr/@tcit/99908779444618462") @@ -55,7 +61,19 @@ defmodule Eventos.Service.Activitypub.ActivitypubTest do assert delete.data["actor"] == event.organizer_actor.url assert delete.data["object"] == event.url - assert Events.get_event_by_url!(event.url) == nil + assert Events.get_event_by_url(event.url) == nil + end + + test "it creates a delete activity and deletes the original comment" do + comment = insert(:comment) + comment = Events.get_comment_full_from_url!(comment.url) + {:ok, delete} = ActivityPub.delete(comment) + + assert delete.data["type"] == "Delete" + assert delete.data["actor"] == comment.actor.url + assert delete.data["object"] == comment.url + + assert Events.get_comment_from_url(comment.url) == nil end end diff --git a/test/eventos_web/controllers/activity_pub_controller_test.exs b/test/eventos_web/controllers/activity_pub_controller_test.exs index 9a716ef3d..73d850cec 100644 --- a/test/eventos_web/controllers/activity_pub_controller_test.exs +++ b/test/eventos_web/controllers/activity_pub_controller_test.exs @@ -3,10 +3,11 @@ defmodule EventosWeb.ActivityPubControllerTest do import Eventos.Factory alias EventosWeb.ActivityPub.{ActorView, ObjectView} alias Eventos.{Repo, Actors, Actors.Actor} + alias Eventos.Service.ActivityPub alias Eventos.Activity import Logger - describe "/@:username" do + describe "/@:preferred_username" do test "it returns a json representation of the actor", %{conn: conn} do actor = insert(:actor) @@ -22,8 +23,8 @@ defmodule EventosWeb.ActivityPubControllerTest do end end - describe "/events/uuid" do - test "it returns a json representation of the object", %{conn: conn} do + describe "/events/:uuid" do + test "it returns a json representation of the event", %{conn: conn} do event = insert(:event) conn = @@ -34,23 +35,60 @@ defmodule EventosWeb.ActivityPubControllerTest do assert json_response(conn, 200) == ObjectView.render("event.json", %{event: event}) Logger.error(inspect(ObjectView.render("event.json", %{event: event}))) end + + test "it returns 404 for non-public events", %{conn: conn} do + event = insert(:event, public: false) + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/events/#{event.uuid}") + + assert json_response(conn, 404) + end end - # describe "/actors/:username/inbox" do - # test "it inserts an incoming activity into the database", %{conn: conn} do - # data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() - # - # conn = - # conn - # |> assign(:valid_signature, true) - # |> put_req_header("content-type", "application/activity+json") - # |> post("/inbox", data) - # - # assert "ok" == json_response(conn, 200) - # :timer.sleep(500) - # assert Activity.get_by_ap_id(data["id"]) - # end - # end + describe "/@:preferred_username/inbox" do + test "it inserts an incoming event into the database", %{conn: conn} do + data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + :timer.sleep(500) + assert ActivityPub.fetch_object_from_url(data["object"]["id"], :note) + end + end + + describe "/@:preferred_username/outbox" do + test "it returns a note activity in a collection", %{conn: conn} do + actor = insert(:actor) + comment = insert(:comment, actor: actor) + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/@#{actor.preferred_username}/outbox") + + assert response(conn, 200) =~ comment.text + end + + test "it returns an event activity in a collection", %{conn: conn} do + actor = insert(:actor) + event = insert(:event, organizer_actor: actor) + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/@#{actor.preferred_username}/outbox") + + assert response(conn, 200) =~ event.title + end + end # describe "/actors/:nickname/followers" do # test "it returns the followers in a collection", %{conn: conn} do @@ -93,43 +131,43 @@ defmodule EventosWeb.ActivityPubControllerTest do # end # end # - # describe "/users/:nickname/following" do + # describe "/@:preferred_username/following" do # test "it returns the following in a collection", %{conn: conn} do - # user = insert(:user) - # user_two = insert(:user) - # User.follow(user, user_two) - # + # actor = insert(:actor) + # actor2 = insert(:actor) + # Eventos.Service.ActivityPub.follow(actor, actor2) + # result = # conn - # |> get("/users/#{user.nickname}/following") + # |> get("/@#{actor.preferred_username}/following") # |> json_response(200) - # - # assert result["first"]["orderedItems"] == [user_two.ap_id] + + # assert result["first"]["orderedItems"] == [actor2.url] # end - # - # test "it works for more than 10 users", %{conn: conn} do - # user = insert(:user) - # + + # test "it works for more than 10 actors", %{conn: conn} do + # actor = insert(:actor) + # Enum.each(1..15, fn _ -> - # user = Repo.get(User, user.id) - # other_user = insert(:user) - # User.follow(user, other_user) + # actor = Repo.get(Actor, actor.id) + # other_actor = insert(:actor) + # Actor.follow(actor, other_actor) # end) - # + # result = # conn - # |> get("/users/#{user.nickname}/following") + # |> get("/@#{actor.preferred_username}/following") # |> json_response(200) - # + # assert length(result["first"]["orderedItems"]) == 10 # assert result["first"]["totalItems"] == 15 # assert result["totalItems"] == 15 - # + # result = # conn - # |> get("/users/#{user.nickname}/following?page=2") + # |> get("/@#{actor.preferred_username}/following?page=2") # |> json_response(200) - # + # assert length(result["orderedItems"]) == 5 # assert result["totalItems"] == 15 # end diff --git a/test/eventos_web/controllers/actor_controller_test.exs b/test/eventos_web/controllers/actor_controller_test.exs index 6406eee4f..3b04bd815 100644 --- a/test/eventos_web/controllers/actor_controller_test.exs +++ b/test/eventos_web/controllers/actor_controller_test.exs @@ -31,6 +31,64 @@ defmodule EventosWeb.ActorControllerTest do end end + describe "show actor" do + test "show existing actor", %{conn: conn, actor: actor} do + actor_id = actor.id + conn = get(conn, actor_path(conn, :show, actor.preferred_username)) + assert %{"data" => %{"id" => actor_id}} = json_response(conn, 200) + end + + test "show non-existing actor", %{conn: conn, actor: actor} do + actor_id = actor.id + conn = get(conn, actor_path(conn, :show, "nonexisting")) + assert "" == response(conn, 404) + end + end + + describe "search for actors" do + test "search for existing actors", %{conn: conn, actor: actor} do + actor_username = actor.preferred_username + conn = get(conn, actor_path(conn, :search, actor_username)) + assert %{"data" => [%{"username" => actor_username}]} = json_response(conn, 200) + end + + test "search for existing actors with similar username", %{conn: conn, actor: actor} do + actor_username = actor.preferred_username + conn = get(conn, actor_path(conn, :search, "thom")) + assert %{"data" => [%{"username" => actor_username}]} = json_response(conn, 200) + end + + test "search for nothing", %{conn: conn, actor: actor} do + actor_username = actor.preferred_username + conn = get(conn, actor_path(conn, :search, "nothing")) + assert %{"data" => []} = json_response(conn, 200) + end + end + + describe "update actor" do + test "update actor with valid attrs", %{conn: conn, user: user, actor: actor} do + conn = auth_conn(conn, user) + + conn = + patch(conn, actor_path(conn, :update, actor.preferred_username), %{ + "actor" => %{"name" => "glouglou"} + }) + + assert %{"data" => %{"display_name" => "glouglou"}} = json_response(conn, 200) + end + + test "update actor with invalid attrs", %{conn: conn, user: user, actor: actor} do + conn = auth_conn(conn, user) + + conn = + patch(conn, actor_path(conn, :update, actor.preferred_username), %{ + "actor" => %{"preferred_username" => nil} + }) + + assert json_response(conn, 422)["errors"] != %{} + end + end + ### # Not possible atm ### diff --git a/test/eventos_web/controllers/address_controller_test.exs b/test/eventos_web/controllers/address_controller_test.exs index c2efc0f25..d0e4c2dde 100644 --- a/test/eventos_web/controllers/address_controller_test.exs +++ b/test/eventos_web/controllers/address_controller_test.exs @@ -34,7 +34,7 @@ defmodule EventosWeb.AddressControllerTest do floor: nil, postalCode: nil, streetAddress: nil, - geom: %{type: nil, data: %{latitude: nil, longitude: nil}} + geom: %{type: "oh no", data: %{latitude: nil, longitude: nil}} } def fixture(:address) do diff --git a/test/eventos_web/controllers/user_controller_test.exs b/test/eventos_web/controllers/user_controller_test.exs index be75411f9..25ce98fda 100644 --- a/test/eventos_web/controllers/user_controller_test.exs +++ b/test/eventos_web/controllers/user_controller_test.exs @@ -5,6 +5,7 @@ defmodule EventosWeb.UserControllerTest do alias Eventos.Actors alias Eventos.Actors.User + use Bamboo.Test @create_attrs %{email: "foo@bar.tld", password: "some password_hash", username: "some username"} # @update_attrs %{email: "foo@fighters.tld", password: "some updated password_hash", username: "some updated username"} @@ -18,7 +19,7 @@ defmodule EventosWeb.UserControllerTest do setup %{conn: conn} do user = insert(:user) actor = insert(:actor, user: user) - {:ok, conn: conn, user: user} + {:ok, conn: conn, user: user, actor: actor} end describe "index" do @@ -31,12 +32,14 @@ defmodule EventosWeb.UserControllerTest do describe "create user" do test "renders user when data is valid", %{conn: conn} do - conn = post(conn, user_path(conn, :create), @create_attrs) + conn = post(conn, user_path(conn, :register), @create_attrs) assert %{"email" => "foo@bar.tld"} = json_response(conn, 201) + assert {:ok, %User{} = user} = Eventos.Actors.get_user_by_email(@create_attrs.email) + assert_delivered_email(Eventos.Email.User.confirmation_email(user)) end test "renders errors when data is invalid", %{conn: conn} do - conn = post(conn, user_path(conn, :create), @invalid_attrs) + conn = post(conn, user_path(conn, :register), @invalid_attrs) assert json_response(conn, 422)["errors"] != %{} end @@ -47,11 +50,139 @@ defmodule EventosWeb.UserControllerTest do username: "framasoft" } - conn = post(conn, user_path(conn, :create), attrs) + conn = post(conn, user_path(conn, :register), attrs) assert %{"email" => "contact@framasoft.org"} = json_response(conn, 201) end end + describe "validating user" do + test "validate user when token is valid", %{conn: conn} do + conn = post(conn, user_path(conn, :create), @create_attrs) + assert %{"email" => "foo@bar.tld"} = json_response(conn, 201) + assert {:ok, %User{} = user} = Eventos.Actors.get_user_by_email(@create_attrs.email) + assert_delivered_email(Eventos.Email.User.confirmation_email(user)) + + conn = get(conn, user_path(conn, :validate, user.confirmation_token)) + assert %{"user" => _, "token" => _} = json_response(conn, 200) + end + + test "validate user when token is invalid", %{conn: conn} do + conn = post(conn, user_path(conn, :create), @create_attrs) + assert %{"email" => "foo@bar.tld"} = json_response(conn, 201) + assert {:ok, %User{} = user} = Eventos.Actors.get_user_by_email(@create_attrs.email) + assert_delivered_email(Eventos.Email.User.confirmation_email(user)) + + conn = get(conn, user_path(conn, :validate, "toto")) + assert %{"error" => _} = json_response(conn, 404) + end + end + + describe "revalidating user" do + test "ask to resend token to user when too soon", %{conn: conn} do + conn = post(conn, user_path(conn, :create), @create_attrs) + assert %{"email" => "foo@bar.tld"} = json_response(conn, 201) + assert {:ok, %User{} = user} = Eventos.Actors.get_user_by_email(@create_attrs.email) + assert_delivered_email(Eventos.Email.User.confirmation_email(user)) + + conn = post(conn, user_path(conn, :resend_confirmation), %{"email" => @create_attrs.email}) + assert %{"error" => _} = json_response(conn, 404) + end + + test "ask to resend token to user when the time is right", %{conn: conn} do + conn = post(conn, user_path(conn, :create), @create_attrs) + + assert %{"email" => "foo@bar.tld"} = json_response(conn, 201) + assert {:ok, %User{} = user} = Eventos.Actors.get_user_by_email(@create_attrs.email) + assert_delivered_email(Eventos.Email.User.confirmation_email(user)) + + # Hammer time ! + {:ok, %User{} = user} = + Eventos.Actors.update_user(user, %{ + confirmation_sent_at: Timex.shift(user.confirmation_sent_at, hours: -3) + }) + + conn = post(conn, user_path(conn, :resend_confirmation), %{"email" => @create_attrs.email}) + assert_delivered_email(Eventos.Email.User.confirmation_email(user)) + assert %{"email" => "foo@bar.tld"} = json_response(conn, 200) + end + end + + describe "resetting user's password" do + test "ask for reset", %{conn: conn, user: user} do + user_email = user.email + + # Send reset email + conn = post(conn, user_path(conn, :send_reset_password), %{"email" => user_email}) + assert {:ok, %User{} = user} = Eventos.Actors.get_user_by_email(user.email) + assert_delivered_email(Eventos.Email.User.reset_password_email(user)) + assert %{"email" => user_email} = json_response(conn, 200) + + # Call reset route + conn = + post(conn, user_path(conn, :reset_password), %{ + "password" => "new password", + "token" => user.reset_password_token + }) + + user_id = user.id + assert %{"user" => %{"id" => user_id}} = json_response(conn, 200) + end + + test "ask twice for reset too soon", %{conn: conn, user: user} do + user_email = user.email + + # Send reset email + conn = post(conn, user_path(conn, :send_reset_password), %{"email" => user.email}) + assert {:ok, %User{} = user} = Eventos.Actors.get_user_by_email(user.email) + assert_delivered_email(Eventos.Email.User.reset_password_email(user)) + assert %{"email" => user_email} = json_response(conn, 200) + + # Send reset email again + conn = post(conn, user_path(conn, :send_reset_password), %{"email" => user.email}) + + assert %{"errors" => "You requested a new reset password too early"} = + json_response(conn, 404) + end + + test "ask twice for reset after a while", %{conn: conn, user: user} do + user_email = user.email + + # Send reset email + conn = post(conn, user_path(conn, :send_reset_password), %{"email" => user.email}) + assert {:ok, %User{} = user} = Eventos.Actors.get_user_by_email(user.email) + assert_delivered_email(Eventos.Email.User.reset_password_email(user)) + assert %{"email" => user_email} = json_response(conn, 200) + + # Hammer time ! + {:ok, %User{} = user} = + Eventos.Actors.update_user(user, %{ + reset_password_sent_at: Timex.shift(user.reset_password_sent_at, hours: -3) + }) + + # Send reset email again + conn = post(conn, user_path(conn, :send_reset_password), %{"email" => user.email}) + assert {:ok, %User{} = user} = Eventos.Actors.get_user_by_email(user.email) + assert_delivered_email(Eventos.Email.User.reset_password_email(user)) + assert %{"email" => user_email} = json_response(conn, 200) + end + + test "ask for reset with wrong address", %{conn: conn} do + conn = post(conn, user_path(conn, :send_reset_password), %{"email" => "yolo@coucou"}) + assert %{"errors" => "Unable to find an user with this email"} = json_response(conn, 404) + end + + test "calling reset route with wrong token", %{conn: conn} do + conn = + post(conn, user_path(conn, :reset_password), %{ + "password" => "new password", + "token" => "just wrong" + }) + + assert %{"errors" => %{"token" => ["Wrong token for password reset"]}} = + json_response(conn, 404) + end + end + # describe "update user" do # setup [:create_user] # diff --git a/test/fixtures/mastodon-post-activity.json b/test/fixtures/mastodon-post-activity.json new file mode 100644 index 000000000..1bee5938b --- /dev/null +++ b/test/fixtures/mastodon-post-activity.json @@ -0,0 +1,65 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "Emoji": "toot:Emoji", + "Hashtag": "as:Hashtag", + "atomUri": "ostatus:atomUri", + "conversation": "ostatus:conversation", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "movedTo": "as:movedTo", + "ostatus": "http://ostatus.org#", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#" + } + ], + "actor": "http://framapiaf.org/users/admin", + "cc": [ + "http://framapiaf.org/users/admin/followers", + "http://eventos.com/@tcit" + ], + "id": "http://framapiaf.org/users/admin/statuses/99512778738411822/activity", + "nickname": "lain", + "object": { + "atomUri": "http://framapiaf.org/users/admin/statuses/99512778738411822", + "attachment": [], + "attributedTo": "http://framapiaf.org/users/admin", + "cc": [ + "http://framapiaf.org/users/admin/followers", + "http://localtesting.pleroma.lol/users/lain" + ], + "content": "

@lain

", + "conversation": "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation", + "id": "http://framapiaf.org/users/admin/statuses/99512778738411822", + "inReplyTo": null, + "inReplyToAtomUri": null, + "published": "2018-02-12T14:08:20Z", + "sensitive": true, + "summary": "cw", + "tag": [ + { + "href": "http://localtesting.pleroma.lol/users/lain", + "name": "@lain@localtesting.pleroma.lol", + "type": "Mention" + } + ], + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Note", + "url": "http://framapiaf.org/@admin/99512778738411822" + }, + "published": "2018-02-12T14:08:20Z", + "signature": { + "created": "2018-02-12T14:08:20Z", + "creator": "http://framapiaf.org/users/admin#main-key", + "signatureValue": "rnNfcopkc6+Ju73P806popcfwrK9wGYHaJVG1/ZvrlEbWVDzaHjkXqj9Q3/xju5l8CSn9tvSgCCtPFqZsFQwn/pFIFUcw7ZWB2xi4bDm3NZ3S4XQ8JRaaX7og5hFxAhWkGhJhAkfxVnOg2hG+w2d/7d7vRVSC1vo5ip4erUaA/PkWusZvPIpxnRWoXaxJsFmVx0gJgjpJkYDyjaXUlp+jmaoseeZ4EPQUWqHLKJ59PRG0mg8j2xAjYH9nQaN14qMRmTGPxY8gfv/CUFcatA+8VJU9KEsJkDAwLVvglydNTLGrxpAJU78a2eaht0foV43XUIZGe3DKiJPgE+UOKGCJw==", + "type": "RsaSignature2017" + }, + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Create" +} diff --git a/test/support/factory.ex b/test/support/factory.ex index 41059a7e3..5246f83bd 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -67,11 +67,14 @@ defmodule Eventos.Factory do end def comment_factory do + uuid = Ecto.UUID.generate() + %Eventos.Events.Comment{ text: "My Comment", actor: build(:actor), event: build(:event), - uuid: Ecto.UUID.generate() + uuid: uuid, + url: "#{EventosWeb.Endpoint.url()}/comments/#{uuid}" } end @@ -86,10 +89,18 @@ defmodule Eventos.Factory do organizer_actor: actor, category: build(:category), physical_address: build(:address), + public: true, url: "#{EventosWeb.Endpoint.url()}/@#{actor.url}/#{Ecto.UUID.generate()}" } end + def participant_factory do + %Eventos.Events.Participant{ + event: build(:event), + actor: build(:actor) + } + end + def session_factory do %Eventos.Events.Session{ title: sequence("MySession"),