diff options
Diffstat (limited to 'test/web')
42 files changed, 2567 insertions, 1384 deletions
diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index 6b5913f95..776ddc8d4 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -815,26 +815,49 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert object["content"] == activity["object"]["content"] end + test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do + user = insert(:user) + + activity = + activity + |> put_in(["object", "type"], "Benis") + + _result = + conn + |> assign(:user, user) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/outbox", activity) + |> json_response(400) + end + test "it inserts an incoming sensitive activity into the database", %{ conn: conn, activity: activity } do user = insert(:user) + conn = assign(conn, :user, user) object = Map.put(activity["object"], "sensitive", true) activity = Map.put(activity, "object", object) - result = + response = conn - |> assign(:user, user) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/outbox", activity) |> json_response(201) - assert Activity.get_by_ap_id(result["id"]) - assert result["object"] - assert %Object{data: object} = Object.normalize(result["object"]) - assert object["sensitive"] == activity["object"]["sensitive"] - assert object["content"] == activity["object"]["content"] + assert Activity.get_by_ap_id(response["id"]) + assert response["object"] + assert %Object{data: response_object} = Object.normalize(response["object"]) + assert response_object["sensitive"] == true + assert response_object["content"] == activity["object"]["content"] + + representation = + conn + |> put_req_header("accept", "application/activity+json") + |> get(response["id"]) + |> json_response(200) + + assert representation["object"]["sensitive"] == true end test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do @@ -1055,12 +1078,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert result["totalItems"] == 15 end - test "returns 403 if requester is not logged in", %{conn: conn} do + test "does not require authentication", %{conn: conn} do user = insert(:user) conn |> get("/users/#{user.nickname}/followers") - |> json_response(403) + |> json_response(200) end end @@ -1152,12 +1175,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert result["totalItems"] == 15 end - test "returns 403 if requester is not logged in", %{conn: conn} do + test "does not require authentication", %{conn: conn} do user = insert(:user) conn |> get("/users/#{user.nickname}/following") - |> json_response(403) + |> json_response(200) end end diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index edd7dfb22..0739cbfef 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -16,11 +16,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.CommonAPI - alias Pleroma.Web.Federator + import ExUnit.CaptureLog + import Mock import Pleroma.Factory import Tesla.Mock - import Mock setup do mock(fn env -> apply(HttpRequestMock, :request, [env]) end) @@ -873,187 +873,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do end end - describe "react to an object" do - test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do - Config.put([:instance, :federating], true) - user = insert(:user) - reactor = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"}) - assert object = Object.normalize(activity) - - {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥") - - assert called(Federator.publish(reaction_activity)) - end - - test "adds an emoji reaction activity to the db" do - user = insert(:user) - reactor = insert(:user) - third_user = insert(:user) - fourth_user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"}) - assert object = Object.normalize(activity) - - {:ok, reaction_activity, object} = ActivityPub.react_with_emoji(reactor, object, "🔥") - - assert reaction_activity - - assert reaction_activity.data["actor"] == reactor.ap_id - assert reaction_activity.data["type"] == "EmojiReact" - assert reaction_activity.data["content"] == "🔥" - assert reaction_activity.data["object"] == object.data["id"] - assert reaction_activity.data["to"] == [User.ap_followers(reactor), activity.data["actor"]] - assert reaction_activity.data["context"] == object.data["context"] - assert object.data["reaction_count"] == 1 - assert object.data["reactions"] == [["🔥", [reactor.ap_id]]] - - {:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(third_user, object, "☕") - - assert object.data["reaction_count"] == 2 - assert object.data["reactions"] == [["🔥", [reactor.ap_id]], ["☕", [third_user.ap_id]]] - - {:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(fourth_user, object, "🔥") - - assert object.data["reaction_count"] == 3 - - assert object.data["reactions"] == [ - ["🔥", [fourth_user.ap_id, reactor.ap_id]], - ["☕", [third_user.ap_id]] - ] - end - - test "reverts emoji reaction on error" do - [user, reactor] = insert_list(2, :user) - - {:ok, activity} = CommonAPI.post(user, %{"status" => "Status"}) - object = Object.normalize(activity) - - with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do - assert {:error, :reverted} = ActivityPub.react_with_emoji(reactor, object, "😀") - end - - object = Object.get_by_ap_id(object.data["id"]) - refute object.data["reaction_count"] - refute object.data["reactions"] - end - end - - describe "unreacting to an object" do - test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do - Config.put([:instance, :federating], true) - user = insert(:user) - reactor = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"}) - assert object = Object.normalize(activity) - - {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥") - - assert called(Federator.publish(reaction_activity)) - - {:ok, unreaction_activity, _object} = - ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"]) - - assert called(Federator.publish(unreaction_activity)) - end - - test "adds an undo activity to the db" do - user = insert(:user) - reactor = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"}) - assert object = Object.normalize(activity) - - {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥") - - {:ok, unreaction_activity, _object} = - ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"]) - - assert unreaction_activity.actor == reactor.ap_id - assert unreaction_activity.data["object"] == reaction_activity.data["id"] - - object = Object.get_by_ap_id(object.data["id"]) - assert object.data["reaction_count"] == 0 - assert object.data["reactions"] == [] - end - - test "reverts emoji unreact on error" do - [user, reactor] = insert_list(2, :user) - {:ok, activity} = CommonAPI.post(user, %{"status" => "Status"}) - object = Object.normalize(activity) - - {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "😀") - - with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do - assert {:error, :reverted} = - ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"]) - end - - object = Object.get_by_ap_id(object.data["id"]) - - assert object.data["reaction_count"] == 1 - assert object.data["reactions"] == [["😀", [reactor.ap_id]]] - end - end - - describe "unliking" do - test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do - Config.put([:instance, :federating], true) - - note_activity = insert(:note_activity) - object = Object.normalize(note_activity) - user = insert(:user) - - {:ok, object} = ActivityPub.unlike(user, object) - refute called(Federator.publish()) - - {:ok, _like_activity} = CommonAPI.favorite(user, note_activity.id) - object = Object.get_by_id(object.id) - assert object.data["like_count"] == 1 - - {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object) - assert object.data["like_count"] == 0 - - assert called(Federator.publish(unlike_activity)) - end - - test "reverts unliking on error" do - note_activity = insert(:note_activity) - user = insert(:user) - - {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id) - object = Object.normalize(note_activity) - assert object.data["like_count"] == 1 - - with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do - assert {:error, :reverted} = ActivityPub.unlike(user, object) - end - - assert Object.get_by_ap_id(object.data["id"]) == object - assert object.data["like_count"] == 1 - assert Activity.get_by_id(like_activity.id) - end - - test "unliking a previously liked object" do - note_activity = insert(:note_activity) - object = Object.normalize(note_activity) - user = insert(:user) - - # Unliking something that hasn't been liked does nothing - {:ok, object} = ActivityPub.unlike(user, object) - assert object.data["like_count"] == 0 - - {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id) - - object = Object.get_by_id(object.id) - assert object.data["like_count"] == 1 - - {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object) - assert object.data["like_count"] == 0 - - assert Activity.get_by_id(like_activity.id) == nil - assert note_activity.actor in unlike_activity.recipients - end - end - describe "announcing an object" do test "adds an announce activity to the db" do note_activity = insert(:note_activity) @@ -1123,52 +942,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do end end - describe "unannouncing an object" do - test "unannouncing a previously announced object" do - note_activity = insert(:note_activity) - object = Object.normalize(note_activity) - user = insert(:user) - - # Unannouncing an object that is not announced does nothing - {:ok, object} = ActivityPub.unannounce(user, object) - refute object.data["announcement_count"] - - {:ok, announce_activity, object} = ActivityPub.announce(user, object) - assert object.data["announcement_count"] == 1 - - {:ok, unannounce_activity, object} = ActivityPub.unannounce(user, object) - assert object.data["announcement_count"] == 0 - - assert unannounce_activity.data["to"] == [ - User.ap_followers(user), - object.data["actor"] - ] - - assert unannounce_activity.data["type"] == "Undo" - assert unannounce_activity.data["object"] == announce_activity.data - assert unannounce_activity.data["actor"] == user.ap_id - assert unannounce_activity.data["context"] == announce_activity.data["context"] - - assert Activity.get_by_id(announce_activity.id) == nil - end - - test "reverts unannouncing on error" do - note_activity = insert(:note_activity) - object = Object.normalize(note_activity) - user = insert(:user) - - {:ok, _announce_activity, object} = ActivityPub.announce(user, object) - assert object.data["announcement_count"] == 1 - - with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do - assert {:error, :reverted} = ActivityPub.unannounce(user, object) - end - - object = Object.get_by_ap_id(object.data["id"]) - assert object.data["announcement_count"] == 1 - end - end - describe "uploading files" do test "copies the file to the configured folder" do file = %Plug.Upload{ @@ -1275,7 +1048,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do end end - describe "blocking / unblocking" do + describe "blocking" do test "reverts block activity on error" do [blocker, blocked] = insert_list(2, :user) @@ -1297,175 +1070,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert activity.data["actor"] == blocker.ap_id assert activity.data["object"] == blocked.ap_id end - - test "reverts unblock activity on error" do - [blocker, blocked] = insert_list(2, :user) - {:ok, block_activity} = ActivityPub.block(blocker, blocked) - - with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do - assert {:error, :reverted} = ActivityPub.unblock(blocker, blocked) - end - - assert block_activity.data["type"] == "Block" - assert block_activity.data["actor"] == blocker.ap_id - - assert Repo.aggregate(Activity, :count, :id) == 1 - assert Repo.aggregate(Object, :count, :id) == 1 - end - - test "creates an undo activity for the last block" do - blocker = insert(:user) - blocked = insert(:user) - - {:ok, block_activity} = ActivityPub.block(blocker, blocked) - {:ok, activity} = ActivityPub.unblock(blocker, blocked) - - assert activity.data["type"] == "Undo" - assert activity.data["actor"] == blocker.ap_id - - embedded_object = activity.data["object"] - assert is_map(embedded_object) - assert embedded_object["type"] == "Block" - assert embedded_object["object"] == blocked.ap_id - assert embedded_object["id"] == block_activity.data["id"] - end - end - - describe "deletion" do - setup do: clear_config([:instance, :rewrite_policy]) - - test "it reverts deletion on error" do - note = insert(:note_activity) - object = Object.normalize(note) - - with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do - assert {:error, :reverted} = ActivityPub.delete(object) - end - - assert Repo.aggregate(Activity, :count, :id) == 1 - assert Repo.get(Object, object.id) == object - assert Activity.get_by_id(note.id) == note - end - - test "it creates a delete activity and deletes the original object" do - note = insert(:note_activity) - object = Object.normalize(note) - {:ok, delete} = ActivityPub.delete(object) - - assert delete.data["type"] == "Delete" - assert delete.data["actor"] == note.data["actor"] - assert delete.data["object"] == object.data["id"] - - assert Activity.get_by_id(delete.id) != nil - - assert Repo.get(Object, object.id).data["type"] == "Tombstone" - end - - test "it doesn't fail when an activity was already deleted" do - {:ok, delete} = insert(:note_activity) |> Object.normalize() |> ActivityPub.delete() - - assert {:ok, ^delete} = delete |> Object.normalize() |> ActivityPub.delete() - end - - test "decrements user note count only for public activities" do - user = insert(:user, note_count: 10) - - {:ok, a1} = - CommonAPI.post(User.get_cached_by_id(user.id), %{ - "status" => "yeah", - "visibility" => "public" - }) - - {:ok, a2} = - CommonAPI.post(User.get_cached_by_id(user.id), %{ - "status" => "yeah", - "visibility" => "unlisted" - }) - - {:ok, a3} = - CommonAPI.post(User.get_cached_by_id(user.id), %{ - "status" => "yeah", - "visibility" => "private" - }) - - {:ok, a4} = - CommonAPI.post(User.get_cached_by_id(user.id), %{ - "status" => "yeah", - "visibility" => "direct" - }) - - {:ok, _} = Object.normalize(a1) |> ActivityPub.delete() - {:ok, _} = Object.normalize(a2) |> ActivityPub.delete() - {:ok, _} = Object.normalize(a3) |> ActivityPub.delete() - {:ok, _} = Object.normalize(a4) |> ActivityPub.delete() - - user = User.get_cached_by_id(user.id) - assert user.note_count == 10 - end - - test "it creates a delete activity and checks that it is also sent to users mentioned by the deleted object" do - user = insert(:user) - note = insert(:note_activity) - object = Object.normalize(note) - - {:ok, object} = - object - |> Object.change(%{ - data: %{ - "actor" => object.data["actor"], - "id" => object.data["id"], - "to" => [user.ap_id], - "type" => "Note" - } - }) - |> Object.update_and_set_cache() - - {:ok, delete} = ActivityPub.delete(object) - - assert user.ap_id in delete.data["to"] - end - - test "decreases reply count" do - user = insert(:user) - user2 = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"}) - reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id} - ap_id = activity.data["id"] - - {:ok, public_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public")) - {:ok, unlisted_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted")) - {:ok, private_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private")) - {:ok, direct_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct")) - - _ = CommonAPI.delete(direct_reply.id, user2) - assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) - assert object.data["repliesCount"] == 2 - - _ = CommonAPI.delete(private_reply.id, user2) - assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) - assert object.data["repliesCount"] == 2 - - _ = CommonAPI.delete(public_reply.id, user2) - assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) - assert object.data["repliesCount"] == 1 - - _ = CommonAPI.delete(unlisted_reply.id, user2) - assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id) - assert object.data["repliesCount"] == 0 - end - - test "it passes delete activity through MRF before deleting the object" do - Pleroma.Config.put([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.DropPolicy) - - note = insert(:note_activity) - object = Object.normalize(note) - - {:error, {:reject, _}} = ActivityPub.delete(object) - - assert Activity.get_by_id(note.id) - assert Repo.get(Object, object.id).data["type"] == object.data["type"] - end end describe "timeline post-processing" do @@ -2403,4 +2007,51 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do u3: %{r1: r3_1.id, r2: r3_2.id}, u4: %{r1: r4_1.id}} end + + describe "maybe_update_follow_information/1" do + setup do + clear_config([:instance, :external_user_synchronization], true) + + user = %{ + local: false, + ap_id: "https://gensokyo.2hu/users/raymoo", + following_address: "https://gensokyo.2hu/users/following", + follower_address: "https://gensokyo.2hu/users/followers", + type: "Person" + } + + %{user: user} + end + + test "logs an error when it can't fetch the info", %{user: user} do + assert capture_log(fn -> + ActivityPub.maybe_update_follow_information(user) + end) =~ "Follower/Following counter update for #{user.ap_id} failed" + end + + test "just returns the input if the user type is Application", %{ + user: user + } do + user = + user + |> Map.put(:type, "Application") + + refute capture_log(fn -> + assert ^user = ActivityPub.maybe_update_follow_information(user) + end) =~ "Follower/Following counter update for #{user.ap_id} failed" + end + + test "it just returns the input if the user has no following/follower addresses", %{ + user: user + } do + user = + user + |> Map.put(:following_address, nil) + |> Map.put(:follower_address, nil) + + refute capture_log(fn -> + assert ^user = ActivityPub.maybe_update_follow_information(user) + end) =~ "Follower/Following counter update for #{user.ap_id} failed" + end + end end diff --git a/test/web/activity_pub/object_validator_test.exs b/test/web/activity_pub/object_validator_test.exs index 3c5c3696e..f382adf3e 100644 --- a/test/web/activity_pub/object_validator_test.exs +++ b/test/web/activity_pub/object_validator_test.exs @@ -1,6 +1,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do use Pleroma.DataCase + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.Utils @@ -8,6 +10,178 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do import Pleroma.Factory + describe "EmojiReacts" do + setup do + user = insert(:user) + {:ok, post_activity} = CommonAPI.post(user, %{"status" => "uguu"}) + + object = Pleroma.Object.get_by_ap_id(post_activity.data["object"]) + + {:ok, valid_emoji_react, []} = Builder.emoji_react(user, object, "👌") + + %{user: user, post_activity: post_activity, valid_emoji_react: valid_emoji_react} + end + + test "it validates a valid EmojiReact", %{valid_emoji_react: valid_emoji_react} do + assert {:ok, _, _} = ObjectValidator.validate(valid_emoji_react, []) + end + + test "it is not valid without a 'content' field", %{valid_emoji_react: valid_emoji_react} do + without_content = + valid_emoji_react + |> Map.delete("content") + + {:error, cng} = ObjectValidator.validate(without_content, []) + + refute cng.valid? + assert {:content, {"can't be blank", [validation: :required]}} in cng.errors + end + + test "it is not valid with a non-emoji content field", %{valid_emoji_react: valid_emoji_react} do + without_emoji_content = + valid_emoji_react + |> Map.put("content", "x") + + {:error, cng} = ObjectValidator.validate(without_emoji_content, []) + + refute cng.valid? + + assert {:content, {"must be a single character emoji", []}} in cng.errors + end + end + + describe "Undos" do + setup do + user = insert(:user) + {:ok, post_activity} = CommonAPI.post(user, %{"status" => "uguu"}) + {:ok, like} = CommonAPI.favorite(user, post_activity.id) + {:ok, valid_like_undo, []} = Builder.undo(user, like) + + %{user: user, like: like, valid_like_undo: valid_like_undo} + end + + test "it validates a basic like undo", %{valid_like_undo: valid_like_undo} do + assert {:ok, _, _} = ObjectValidator.validate(valid_like_undo, []) + end + + test "it does not validate if the actor of the undo is not the actor of the object", %{ + valid_like_undo: valid_like_undo + } do + other_user = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo") + + bad_actor = + valid_like_undo + |> Map.put("actor", other_user.ap_id) + + {:error, cng} = ObjectValidator.validate(bad_actor, []) + + assert {:actor, {"not the same as object actor", []}} in cng.errors + end + + test "it does not validate if the object is missing", %{valid_like_undo: valid_like_undo} do + missing_object = + valid_like_undo + |> Map.put("object", "https://gensokyo.2hu/objects/1") + + {:error, cng} = ObjectValidator.validate(missing_object, []) + + assert {:object, {"can't find object", []}} in cng.errors + assert length(cng.errors) == 1 + end + end + + describe "deletes" do + setup do + user = insert(:user) + {:ok, post_activity} = CommonAPI.post(user, %{"status" => "cancel me daddy"}) + + {:ok, valid_post_delete, _} = Builder.delete(user, post_activity.data["object"]) + {:ok, valid_user_delete, _} = Builder.delete(user, user.ap_id) + + %{user: user, valid_post_delete: valid_post_delete, valid_user_delete: valid_user_delete} + end + + test "it is valid for a post deletion", %{valid_post_delete: valid_post_delete} do + {:ok, valid_post_delete, _} = ObjectValidator.validate(valid_post_delete, []) + + assert valid_post_delete["deleted_activity_id"] + end + + test "it is invalid if the object isn't in a list of certain types", %{ + valid_post_delete: valid_post_delete + } do + object = Object.get_by_ap_id(valid_post_delete["object"]) + + data = + object.data + |> Map.put("type", "Like") + + {:ok, _object} = + object + |> Ecto.Changeset.change(%{data: data}) + |> Object.update_and_set_cache() + + {:error, cng} = ObjectValidator.validate(valid_post_delete, []) + assert {:object, {"object not in allowed types", []}} in cng.errors + end + + test "it is valid for a user deletion", %{valid_user_delete: valid_user_delete} do + assert match?({:ok, _, _}, ObjectValidator.validate(valid_user_delete, [])) + end + + test "it's invalid if the id is missing", %{valid_post_delete: valid_post_delete} do + no_id = + valid_post_delete + |> Map.delete("id") + + {:error, cng} = ObjectValidator.validate(no_id, []) + + assert {:id, {"can't be blank", [validation: :required]}} in cng.errors + end + + test "it's invalid if the object doesn't exist", %{valid_post_delete: valid_post_delete} do + missing_object = + valid_post_delete + |> Map.put("object", "http://does.not/exist") + + {:error, cng} = ObjectValidator.validate(missing_object, []) + + assert {:object, {"can't find object", []}} in cng.errors + end + + test "it's invalid if the actor of the object and the actor of delete are from different domains", + %{valid_post_delete: valid_post_delete} do + valid_user = insert(:user) + + valid_other_actor = + valid_post_delete + |> Map.put("actor", valid_user.ap_id) + + assert match?({:ok, _, _}, ObjectValidator.validate(valid_other_actor, [])) + + invalid_other_actor = + valid_post_delete + |> Map.put("actor", "https://gensokyo.2hu/users/raymoo") + + {:error, cng} = ObjectValidator.validate(invalid_other_actor, []) + + assert {:actor, {"is not allowed to delete object", []}} in cng.errors + end + + test "it's valid if the actor of the object is a local superuser", + %{valid_post_delete: valid_post_delete} do + user = + insert(:user, local: true, is_moderator: true, ap_id: "https://gensokyo.2hu/users/raymoo") + + valid_other_actor = + valid_post_delete + |> Map.put("actor", user.ap_id) + + {:ok, _, meta} = ObjectValidator.validate(valid_other_actor, []) + assert meta[:do_not_federate] + end + end + describe "likes" do setup do user = insert(:user) @@ -36,6 +210,32 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do assert LikeValidator.cast_and_validate(valid_like).valid? end + test "sets the 'to' field to the object actor if no recipients are given", %{ + valid_like: valid_like, + user: user + } do + without_recipients = + valid_like + |> Map.delete("to") + + {:ok, object, _meta} = ObjectValidator.validate(without_recipients, []) + + assert object["to"] == [user.ap_id] + end + + test "sets the context field to the context of the object if no context is given", %{ + valid_like: valid_like, + post_activity: post_activity + } do + without_context = + valid_like + |> Map.delete("context") + + {:ok, object, _meta} = ObjectValidator.validate(without_context, []) + + assert object["context"] == post_activity.data["context"] + end + test "it errors when the actor is missing or not known", %{valid_like: valid_like} do without_actor = Map.delete(valid_like, "actor") diff --git a/test/web/activity_pub/object_validators/types/recipients_test.exs b/test/web/activity_pub/object_validators/types/recipients_test.exs new file mode 100644 index 000000000..f278f039b --- /dev/null +++ b/test/web/activity_pub/object_validators/types/recipients_test.exs @@ -0,0 +1,27 @@ +defmodule Pleroma.Web.ObjectValidators.Types.RecipientsTest do + alias Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients + use Pleroma.DataCase + + test "it asserts that all elements of the list are object ids" do + list = ["https://lain.com/users/lain", "invalid"] + + assert :error == Recipients.cast(list) + end + + test "it works with a list" do + list = ["https://lain.com/users/lain"] + assert {:ok, list} == Recipients.cast(list) + end + + test "it works with a list with whole objects" do + list = ["https://lain.com/users/lain", %{"id" => "https://gensokyo.2hu/users/raymoo"}] + resulting_list = ["https://gensokyo.2hu/users/raymoo", "https://lain.com/users/lain"] + assert {:ok, resulting_list} == Recipients.cast(list) + end + + test "it turns a single string into a list" do + recipient = "https://lain.com/users/lain" + + assert {:ok, [recipient]} == Recipients.cast(recipient) + end +end diff --git a/test/web/activity_pub/side_effects_test.exs b/test/web/activity_pub/side_effects_test.exs index 0b6b55156..b29a7a7be 100644 --- a/test/web/activity_pub/side_effects_test.exs +++ b/test/web/activity_pub/side_effects_test.exs @@ -3,17 +3,201 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.SideEffectsTest do + use Oban.Testing, repo: Pleroma.Repo use Pleroma.DataCase + alias Pleroma.Activity alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.SideEffects alias Pleroma.Web.CommonAPI import Pleroma.Factory + import Mock + + describe "delete objects" do + setup do + user = insert(:user) + other_user = insert(:user) + + {:ok, op} = CommonAPI.post(other_user, %{"status" => "big oof"}) + {:ok, post} = CommonAPI.post(user, %{"status" => "hey", "in_reply_to_id" => op}) + object = Object.normalize(post) + {:ok, delete_data, _meta} = Builder.delete(user, object.data["id"]) + {:ok, delete_user_data, _meta} = Builder.delete(user, user.ap_id) + {:ok, delete, _meta} = ActivityPub.persist(delete_data, local: true) + {:ok, delete_user, _meta} = ActivityPub.persist(delete_user_data, local: true) + %{user: user, delete: delete, post: post, object: object, delete_user: delete_user, op: op} + end + + test "it handles object deletions", %{ + delete: delete, + post: post, + object: object, + user: user, + op: op + } do + with_mock Pleroma.Web.ActivityPub.ActivityPub, [:passthrough], + stream_out: fn _ -> nil end, + stream_out_participations: fn _, _ -> nil end do + {:ok, delete, _} = SideEffects.handle(delete) + user = User.get_cached_by_ap_id(object.data["actor"]) + + assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out(delete)) + assert called(Pleroma.Web.ActivityPub.ActivityPub.stream_out_participations(object, user)) + end + + object = Object.get_by_id(object.id) + assert object.data["type"] == "Tombstone" + refute Activity.get_by_id(post.id) + + user = User.get_by_id(user.id) + assert user.note_count == 0 + + object = Object.normalize(op.data["object"], false) + + assert object.data["repliesCount"] == 0 + end + + test "it handles user deletions", %{delete_user: delete, user: user} do + {:ok, _delete, _} = SideEffects.handle(delete) + ObanHelpers.perform_all() + + assert User.get_cached_by_ap_id(user.ap_id).deactivated + end + end + + describe "EmojiReact objects" do + setup do + poster = insert(:user) + user = insert(:user) + + {:ok, post} = CommonAPI.post(poster, %{"status" => "hey"}) + + {:ok, emoji_react_data, []} = Builder.emoji_react(user, post.object, "👌") + {:ok, emoji_react, _meta} = ActivityPub.persist(emoji_react_data, local: true) + + %{emoji_react: emoji_react, user: user, poster: poster} + end + + test "adds the reaction to the object", %{emoji_react: emoji_react, user: user} do + {:ok, emoji_react, _} = SideEffects.handle(emoji_react) + object = Object.get_by_ap_id(emoji_react.data["object"]) + + assert object.data["reaction_count"] == 1 + assert ["👌", [user.ap_id]] in object.data["reactions"] + end + + test "creates a notification", %{emoji_react: emoji_react, poster: poster} do + {:ok, emoji_react, _} = SideEffects.handle(emoji_react) + assert Repo.get_by(Notification, user_id: poster.id, activity_id: emoji_react.id) + end + end + + describe "Undo objects" do + setup do + poster = insert(:user) + user = insert(:user) + {:ok, post} = CommonAPI.post(poster, %{"status" => "hey"}) + {:ok, like} = CommonAPI.favorite(user, post.id) + {:ok, reaction} = CommonAPI.react_with_emoji(post.id, user, "👍") + {:ok, announce, _} = CommonAPI.repeat(post.id, user) + {:ok, block} = ActivityPub.block(user, poster) + User.block(user, poster) + + {:ok, undo_data, _meta} = Builder.undo(user, like) + {:ok, like_undo, _meta} = ActivityPub.persist(undo_data, local: true) + + {:ok, undo_data, _meta} = Builder.undo(user, reaction) + {:ok, reaction_undo, _meta} = ActivityPub.persist(undo_data, local: true) + + {:ok, undo_data, _meta} = Builder.undo(user, announce) + {:ok, announce_undo, _meta} = ActivityPub.persist(undo_data, local: true) + + {:ok, undo_data, _meta} = Builder.undo(user, block) + {:ok, block_undo, _meta} = ActivityPub.persist(undo_data, local: true) + + %{ + like_undo: like_undo, + post: post, + like: like, + reaction_undo: reaction_undo, + reaction: reaction, + announce_undo: announce_undo, + announce: announce, + block_undo: block_undo, + block: block, + poster: poster, + user: user + } + end + + test "deletes the original block", %{block_undo: block_undo, block: block} do + {:ok, _block_undo, _} = SideEffects.handle(block_undo) + refute Activity.get_by_id(block.id) + end + + test "unblocks the blocked user", %{block_undo: block_undo, block: block} do + blocker = User.get_by_ap_id(block.data["actor"]) + blocked = User.get_by_ap_id(block.data["object"]) + + {:ok, _block_undo, _} = SideEffects.handle(block_undo) + refute User.blocks?(blocker, blocked) + end + + test "an announce undo removes the announce from the object", %{ + announce_undo: announce_undo, + post: post + } do + {:ok, _announce_undo, _} = SideEffects.handle(announce_undo) + + object = Object.get_by_ap_id(post.data["object"]) + + assert object.data["announcement_count"] == 0 + assert object.data["announcements"] == [] + end + + test "deletes the original announce", %{announce_undo: announce_undo, announce: announce} do + {:ok, _announce_undo, _} = SideEffects.handle(announce_undo) + refute Activity.get_by_id(announce.id) + end + + test "a reaction undo removes the reaction from the object", %{ + reaction_undo: reaction_undo, + post: post + } do + {:ok, _reaction_undo, _} = SideEffects.handle(reaction_undo) + + object = Object.get_by_ap_id(post.data["object"]) + + assert object.data["reaction_count"] == 0 + assert object.data["reactions"] == [] + end + + test "deletes the original reaction", %{reaction_undo: reaction_undo, reaction: reaction} do + {:ok, _reaction_undo, _} = SideEffects.handle(reaction_undo) + refute Activity.get_by_id(reaction.id) + end + + test "a like undo removes the like from the object", %{like_undo: like_undo, post: post} do + {:ok, _like_undo, _} = SideEffects.handle(like_undo) + + object = Object.get_by_ap_id(post.data["object"]) + + assert object.data["like_count"] == 0 + assert object.data["likes"] == [] + end + + test "deletes the original like", %{like_undo: like_undo, like: like} do + {:ok, _like_undo, _} = SideEffects.handle(like_undo) + refute Activity.get_by_id(like.id) + end + end describe "like objects" do setup do diff --git a/test/web/activity_pub/transmogrifier/delete_handling_test.exs b/test/web/activity_pub/transmogrifier/delete_handling_test.exs new file mode 100644 index 000000000..f235a8e63 --- /dev/null +++ b/test/web/activity_pub/transmogrifier/delete_handling_test.exs @@ -0,0 +1,86 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.DeleteHandlingTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Tests.ObanHelpers + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Factory + + setup_all do + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + :ok + end + + test "it works for incoming deletes" do + activity = insert(:note_activity) + deleting_user = insert(:user) + + data = + File.read!("test/fixtures/mastodon-delete.json") + |> Poison.decode!() + |> Map.put("actor", deleting_user.ap_id) + |> put_in(["object", "id"], activity.data["object"]) + + {:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} = + Transmogrifier.handle_incoming(data) + + assert id == data["id"] + + # We delete the Create activity because we base our timelines on it. + # This should be changed after we unify objects and activities + refute Activity.get_by_id(activity.id) + assert actor == deleting_user.ap_id + + # Objects are replaced by a tombstone object. + object = Object.normalize(activity.data["object"]) + assert object.data["type"] == "Tombstone" + end + + test "it fails for incoming deletes with spoofed origin" do + activity = insert(:note_activity) + %{ap_id: ap_id} = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo") + + data = + File.read!("test/fixtures/mastodon-delete.json") + |> Poison.decode!() + |> Map.put("actor", ap_id) + |> put_in(["object", "id"], activity.data["object"]) + + assert match?({:error, _}, Transmogrifier.handle_incoming(data)) + end + + @tag capture_log: true + test "it works for incoming user deletes" do + %{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin") + + data = + File.read!("test/fixtures/mastodon-delete-user.json") + |> Poison.decode!() + + {:ok, _} = Transmogrifier.handle_incoming(data) + ObanHelpers.perform_all() + + assert User.get_cached_by_ap_id(ap_id).deactivated + end + + test "it fails for incoming user deletes with spoofed origin" do + %{ap_id: ap_id} = insert(:user) + + data = + File.read!("test/fixtures/mastodon-delete-user.json") + |> Poison.decode!() + |> Map.put("actor", ap_id) + + assert match?({:error, _}, Transmogrifier.handle_incoming(data)) + + assert User.get_cached_by_ap_id(ap_id) + end +end diff --git a/test/web/activity_pub/transmogrifier/emoji_react_handling_test.exs b/test/web/activity_pub/transmogrifier/emoji_react_handling_test.exs new file mode 100644 index 000000000..6988e3e0a --- /dev/null +++ b/test/web/activity_pub/transmogrifier/emoji_react_handling_test.exs @@ -0,0 +1,61 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "it works for incoming emoji reactions" do + user = insert(:user) + other_user = insert(:user, local: false) + {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"}) + + data = + File.read!("test/fixtures/emoji-reaction.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + |> Map.put("actor", other_user.ap_id) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == other_user.ap_id + assert data["type"] == "EmojiReact" + assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2" + assert data["object"] == activity.data["object"] + assert data["content"] == "👌" + + object = Object.get_by_ap_id(data["object"]) + + assert object.data["reaction_count"] == 1 + assert match?([["👌", _]], object.data["reactions"]) + end + + test "it reject invalid emoji reactions" do + user = insert(:user) + other_user = insert(:user, local: false) + {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"}) + + data = + File.read!("test/fixtures/emoji-reaction-too-long.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + |> Map.put("actor", other_user.ap_id) + + assert {:error, _} = Transmogrifier.handle_incoming(data) + + data = + File.read!("test/fixtures/emoji-reaction-no-emoji.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + |> Map.put("actor", other_user.ap_id) + + assert {:error, _} = Transmogrifier.handle_incoming(data) + end +end diff --git a/test/web/activity_pub/transmogrifier/like_handling_test.exs b/test/web/activity_pub/transmogrifier/like_handling_test.exs new file mode 100644 index 000000000..54a5c1dbc --- /dev/null +++ b/test/web/activity_pub/transmogrifier/like_handling_test.exs @@ -0,0 +1,78 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.LikeHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "it works for incoming likes" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"}) + + data = + File.read!("test/fixtures/mastodon-like.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + _actor = insert(:user, ap_id: data["actor"], local: false) + + {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) + + refute Enum.empty?(activity.recipients) + + assert data["actor"] == "http://mastodon.example.org/users/admin" + assert data["type"] == "Like" + assert data["id"] == "http://mastodon.example.org/users/admin#likes/2" + assert data["object"] == activity.data["object"] + end + + test "it works for incoming misskey likes, turning them into EmojiReacts" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"}) + + data = + File.read!("test/fixtures/misskey-like.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + _actor = insert(:user, ap_id: data["actor"], local: false) + + {:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data) + + assert activity_data["actor"] == data["actor"] + assert activity_data["type"] == "EmojiReact" + assert activity_data["id"] == data["id"] + assert activity_data["object"] == activity.data["object"] + assert activity_data["content"] == "🍮" + end + + test "it works for incoming misskey likes that contain unicode emojis, turning them into EmojiReacts" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"}) + + data = + File.read!("test/fixtures/misskey-like.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + |> Map.put("_misskey_reaction", "⭐") + + _actor = insert(:user, ap_id: data["actor"], local: false) + + {:ok, %Activity{data: activity_data, local: false}} = Transmogrifier.handle_incoming(data) + + assert activity_data["actor"] == data["actor"] + assert activity_data["type"] == "EmojiReact" + assert activity_data["id"] == data["id"] + assert activity_data["object"] == activity.data["object"] + assert activity_data["content"] == "⭐" + end +end diff --git a/test/web/activity_pub/transmogrifier/undo_handling_test.exs b/test/web/activity_pub/transmogrifier/undo_handling_test.exs new file mode 100644 index 000000000..eaf58adf7 --- /dev/null +++ b/test/web/activity_pub/transmogrifier/undo_handling_test.exs @@ -0,0 +1,185 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.UndoHandlingTest do + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "it works for incoming emoji reaction undos" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"}) + {:ok, reaction_activity} = CommonAPI.react_with_emoji(activity.id, user, "👌") + + data = + File.read!("test/fixtures/mastodon-undo-like.json") + |> Poison.decode!() + |> Map.put("object", reaction_activity.data["id"]) + |> Map.put("actor", user.ap_id) + + {:ok, activity} = Transmogrifier.handle_incoming(data) + + assert activity.actor == user.ap_id + assert activity.data["id"] == data["id"] + assert activity.data["type"] == "Undo" + end + + test "it returns an error for incoming unlikes wihout a like activity" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"}) + + data = + File.read!("test/fixtures/mastodon-undo-like.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + assert Transmogrifier.handle_incoming(data) == :error + end + + test "it works for incoming unlikes with an existing like activity" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"}) + + like_data = + File.read!("test/fixtures/mastodon-like.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + _liker = insert(:user, ap_id: like_data["actor"], local: false) + + {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data) + + data = + File.read!("test/fixtures/mastodon-undo-like.json") + |> Poison.decode!() + |> Map.put("object", like_data) + |> Map.put("actor", like_data["actor"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "http://mastodon.example.org/users/admin" + assert data["type"] == "Undo" + assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo" + assert data["object"] == "http://mastodon.example.org/users/admin#likes/2" + + note = Object.get_by_ap_id(like_data["object"]) + assert note.data["like_count"] == 0 + assert note.data["likes"] == [] + end + + test "it works for incoming unlikes with an existing like activity and a compact object" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"}) + + like_data = + File.read!("test/fixtures/mastodon-like.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + _liker = insert(:user, ap_id: like_data["actor"], local: false) + + {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data) + + data = + File.read!("test/fixtures/mastodon-undo-like.json") + |> Poison.decode!() + |> Map.put("object", like_data["id"]) + |> Map.put("actor", like_data["actor"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == "http://mastodon.example.org/users/admin" + assert data["type"] == "Undo" + assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo" + assert data["object"] == "http://mastodon.example.org/users/admin#likes/2" + end + + test "it works for incoming unannounces with an existing notice" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"}) + + announce_data = + File.read!("test/fixtures/mastodon-announce.json") + |> Poison.decode!() + |> Map.put("object", activity.data["object"]) + + _announcer = insert(:user, ap_id: announce_data["actor"], local: false) + + {:ok, %Activity{data: announce_data, local: false}} = + Transmogrifier.handle_incoming(announce_data) + + data = + File.read!("test/fixtures/mastodon-undo-announce.json") + |> Poison.decode!() + |> Map.put("object", announce_data) + |> Map.put("actor", announce_data["actor"]) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["type"] == "Undo" + + assert data["object"] == + "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" + end + + test "it works for incomming unfollows with an existing follow" do + user = insert(:user) + + follow_data = + File.read!("test/fixtures/mastodon-follow-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + _follower = insert(:user, ap_id: follow_data["actor"], local: false) + + {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data) + + data = + File.read!("test/fixtures/mastodon-unfollow-activity.json") + |> Poison.decode!() + |> Map.put("object", follow_data) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["type"] == "Undo" + assert data["object"]["type"] == "Follow" + assert data["object"]["object"] == user.ap_id + assert data["actor"] == "http://mastodon.example.org/users/admin" + + refute User.following?(User.get_cached_by_ap_id(data["actor"]), user) + end + + test "it works for incoming unblocks with an existing block" do + user = insert(:user) + + block_data = + File.read!("test/fixtures/mastodon-block-activity.json") + |> Poison.decode!() + |> Map.put("object", user.ap_id) + + _blocker = insert(:user, ap_id: block_data["actor"], local: false) + + {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(block_data) + + data = + File.read!("test/fixtures/mastodon-unblock-activity.json") + |> Poison.decode!() + |> Map.put("object", block_data) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + assert data["type"] == "Undo" + assert data["object"] == block_data["id"] + + blocker = User.get_cached_by_ap_id(data["actor"]) + + refute User.blocks?(blocker, user) + end +end diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index 10d86ee45..2914c90ea 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -325,180 +325,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert object_data["cc"] == to end - test "it works for incoming likes" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"}) - - data = - File.read!("test/fixtures/mastodon-like.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - {:ok, %Activity{data: data, local: false} = activity} = Transmogrifier.handle_incoming(data) - - refute Enum.empty?(activity.recipients) - - assert data["actor"] == "http://mastodon.example.org/users/admin" - assert data["type"] == "Like" - assert data["id"] == "http://mastodon.example.org/users/admin#likes/2" - assert data["object"] == activity.data["object"] - end - - test "it works for incoming misskey likes, turning them into EmojiReacts" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"}) - - data = - File.read!("test/fixtures/misskey-like.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == data["actor"] - assert data["type"] == "EmojiReact" - assert data["id"] == data["id"] - assert data["object"] == activity.data["object"] - assert data["content"] == "🍮" - end - - test "it works for incoming misskey likes that contain unicode emojis, turning them into EmojiReacts" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"}) - - data = - File.read!("test/fixtures/misskey-like.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - |> Map.put("_misskey_reaction", "⭐") - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == data["actor"] - assert data["type"] == "EmojiReact" - assert data["id"] == data["id"] - assert data["object"] == activity.data["object"] - assert data["content"] == "⭐" - end - - test "it works for incoming emoji reactions" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"}) - - data = - File.read!("test/fixtures/emoji-reaction.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "http://mastodon.example.org/users/admin" - assert data["type"] == "EmojiReact" - assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2" - assert data["object"] == activity.data["object"] - assert data["content"] == "👌" - end - - test "it reject invalid emoji reactions" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"}) - - data = - File.read!("test/fixtures/emoji-reaction-too-long.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - assert :error = Transmogrifier.handle_incoming(data) - - data = - File.read!("test/fixtures/emoji-reaction-no-emoji.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - assert :error = Transmogrifier.handle_incoming(data) - end - - test "it works for incoming emoji reaction undos" do - user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"}) - {:ok, reaction_activity, _object} = CommonAPI.react_with_emoji(activity.id, user, "👌") - - data = - File.read!("test/fixtures/mastodon-undo-like.json") - |> Poison.decode!() - |> Map.put("object", reaction_activity.data["id"]) - |> Map.put("actor", user.ap_id) - - {:ok, activity} = Transmogrifier.handle_incoming(data) - - assert activity.actor == user.ap_id - assert activity.data["id"] == data["id"] - assert activity.data["type"] == "Undo" - end - - test "it returns an error for incoming unlikes wihout a like activity" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"}) - - data = - File.read!("test/fixtures/mastodon-undo-like.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - assert Transmogrifier.handle_incoming(data) == :error - end - - test "it works for incoming unlikes with an existing like activity" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"}) - - like_data = - File.read!("test/fixtures/mastodon-like.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data) - - data = - File.read!("test/fixtures/mastodon-undo-like.json") - |> Poison.decode!() - |> Map.put("object", like_data) - |> Map.put("actor", like_data["actor"]) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "http://mastodon.example.org/users/admin" - assert data["type"] == "Undo" - assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo" - assert data["object"]["id"] == "http://mastodon.example.org/users/admin#likes/2" - end - - test "it works for incoming unlikes with an existing like activity and a compact object" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"}) - - like_data = - File.read!("test/fixtures/mastodon-like.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - {:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data) - - data = - File.read!("test/fixtures/mastodon-undo-like.json") - |> Poison.decode!() - |> Map.put("object", like_data["id"]) - |> Map.put("actor", like_data["actor"]) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["actor"] == "http://mastodon.example.org/users/admin" - assert data["type"] == "Undo" - assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo" - assert data["object"]["id"] == "http://mastodon.example.org/users/admin#likes/2" - end - test "it works for incoming announces" do data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!() @@ -655,7 +481,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do test "it strips internal reactions" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"}) - {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "📢") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "📢") %{object: object} = Activity.get_by_id_with_object(activity.id) assert Map.has_key?(object.data, "reactions") @@ -822,112 +648,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert user.locked == true end - test "it works for incoming deletes" do - activity = insert(:note_activity) - deleting_user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-delete.json") - |> Poison.decode!() - - object = - data["object"] - |> Map.put("id", activity.data["object"]) - - data = - data - |> Map.put("object", object) - |> Map.put("actor", deleting_user.ap_id) - - {:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} = - Transmogrifier.handle_incoming(data) - - assert id == data["id"] - refute Activity.get_by_id(activity.id) - assert actor == deleting_user.ap_id - end - - test "it fails for incoming deletes with spoofed origin" do - activity = insert(:note_activity) - - data = - File.read!("test/fixtures/mastodon-delete.json") - |> Poison.decode!() - - object = - data["object"] - |> Map.put("id", activity.data["object"]) - - data = - data - |> Map.put("object", object) - - assert capture_log(fn -> - :error = Transmogrifier.handle_incoming(data) - end) =~ - "[error] Could not decode user at fetch http://mastodon.example.org/users/gargron, {:error, :nxdomain}" - - assert Activity.get_by_id(activity.id) - end - - @tag capture_log: true - test "it works for incoming user deletes" do - %{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin") - - data = - File.read!("test/fixtures/mastodon-delete-user.json") - |> Poison.decode!() - - {:ok, _} = Transmogrifier.handle_incoming(data) - ObanHelpers.perform_all() - - refute User.get_cached_by_ap_id(ap_id) - end - - test "it fails for incoming user deletes with spoofed origin" do - %{ap_id: ap_id} = insert(:user) - - data = - File.read!("test/fixtures/mastodon-delete-user.json") - |> Poison.decode!() - |> Map.put("actor", ap_id) - - assert capture_log(fn -> - assert :error == Transmogrifier.handle_incoming(data) - end) =~ "Object containment failed" - - assert User.get_cached_by_ap_id(ap_id) - end - - test "it works for incoming unannounces with an existing notice" do - user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"}) - - announce_data = - File.read!("test/fixtures/mastodon-announce.json") - |> Poison.decode!() - |> Map.put("object", activity.data["object"]) - - {:ok, %Activity{data: announce_data, local: false}} = - Transmogrifier.handle_incoming(announce_data) - - data = - File.read!("test/fixtures/mastodon-undo-announce.json") - |> Poison.decode!() - |> Map.put("object", announce_data) - |> Map.put("actor", announce_data["actor"]) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - - assert data["type"] == "Undo" - assert object_data = data["object"] - assert object_data["type"] == "Announce" - assert object_data["object"] == activity.data["object"] - - assert object_data["id"] == - "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity" - end - test "it works for incomming unfollows with an existing follow" do user = insert(:user) @@ -1022,32 +742,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do refute User.following?(blocked, blocker) end - test "it works for incoming unblocks with an existing block" do - user = insert(:user) - - block_data = - File.read!("test/fixtures/mastodon-block-activity.json") - |> Poison.decode!() - |> Map.put("object", user.ap_id) - - {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(block_data) - - data = - File.read!("test/fixtures/mastodon-unblock-activity.json") - |> Poison.decode!() - |> Map.put("object", block_data) - - {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) - assert data["type"] == "Undo" - assert data["object"]["type"] == "Block" - assert data["object"]["object"] == user.ap_id - assert data["actor"] == "http://mastodon.example.org/users/admin" - - blocker = User.get_cached_by_ap_id(data["actor"]) - - refute User.blocks?(blocker, user) - end - test "it works for incoming accepts which were pre-accepted" do follower = insert(:user) followed = insert(:user) diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs index b0bfed917..b8d811c73 100644 --- a/test/web/activity_pub/utils_test.exs +++ b/test/web/activity_pub/utils_test.exs @@ -102,34 +102,6 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do end end - describe "make_unlike_data/3" do - test "returns data for unlike activity" do - user = insert(:user) - like_activity = insert(:like_activity, data_attrs: %{"context" => "test context"}) - - object = Object.normalize(like_activity.data["object"]) - - assert Utils.make_unlike_data(user, like_activity, nil) == %{ - "type" => "Undo", - "actor" => user.ap_id, - "object" => like_activity.data, - "to" => [user.follower_address, object.data["actor"]], - "cc" => [Pleroma.Constants.as_public()], - "context" => like_activity.data["context"] - } - - assert Utils.make_unlike_data(user, like_activity, "9mJEZK0tky1w2xD2vY") == %{ - "type" => "Undo", - "actor" => user.ap_id, - "object" => like_activity.data, - "to" => [user.follower_address, object.data["actor"]], - "cc" => [Pleroma.Constants.as_public()], - "context" => like_activity.data["context"], - "id" => "9mJEZK0tky1w2xD2vY" - } - end - end - describe "make_like_data" do setup do user = insert(:user) diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 1862a9589..4697af50e 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -6,19 +6,22 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do use Pleroma.Web.ConnCase use Oban.Testing, repo: Pleroma.Repo - import Pleroma.Factory import ExUnit.CaptureLog + import Mock + import Pleroma.Factory alias Pleroma.Activity alias Pleroma.Config alias Pleroma.ConfigDB alias Pleroma.HTML + alias Pleroma.MFA alias Pleroma.ModerationLog alias Pleroma.Repo alias Pleroma.ReportNote alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.UserInviteToken + alias Pleroma.Web alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.CommonAPI alias Pleroma.Web.MediaProxy @@ -146,17 +149,26 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do test "single user", %{admin: admin, conn: conn} do user = insert(:user) - conn = - conn - |> put_req_header("accept", "application/json") - |> delete("/api/pleroma/admin/users?nickname=#{user.nickname}") + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + conn = + conn + |> put_req_header("accept", "application/json") + |> delete("/api/pleroma/admin/users?nickname=#{user.nickname}") - log_entry = Repo.one(ModerationLog) + ObanHelpers.perform_all() - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} deleted users: @#{user.nickname}" + assert User.get_by_nickname(user.nickname).deactivated + + log_entry = Repo.one(ModerationLog) - assert json_response(conn, 200) == user.nickname + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deleted users: @#{user.nickname}" + + assert json_response(conn, 200) == [user.nickname] + + assert called(Pleroma.Web.Federator.publish(:_)) + end end test "multiple users", %{admin: admin, conn: conn} do @@ -737,6 +749,39 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do } end + test "pagination works correctly with service users", %{conn: conn} do + service1 = insert(:user, ap_id: Web.base_url() <> "/relay") + service2 = insert(:user, ap_id: Web.base_url() <> "/internal/fetch") + insert_list(25, :user) + + assert %{"count" => 26, "page_size" => 10, "users" => users1} = + conn + |> get("/api/pleroma/admin/users?page=1&filters=", %{page_size: "10"}) + |> json_response(200) + + assert Enum.count(users1) == 10 + assert service1 not in [users1] + assert service2 not in [users1] + + assert %{"count" => 26, "page_size" => 10, "users" => users2} = + conn + |> get("/api/pleroma/admin/users?page=2&filters=", %{page_size: "10"}) + |> json_response(200) + + assert Enum.count(users2) == 10 + assert service1 not in [users2] + assert service2 not in [users2] + + assert %{"count" => 26, "page_size" => 10, "users" => users3} = + conn + |> get("/api/pleroma/admin/users?page=3&filters=", %{page_size: "10"}) + |> json_response(200) + + assert Enum.count(users3) == 6 + assert service1 not in [users3] + assert service2 not in [users3] + end + test "renders empty array for the second page", %{conn: conn} do insert(:user) @@ -1234,6 +1279,38 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do "@#{admin.nickname} deactivated users: @#{user.nickname}" end + describe "PUT disable_mfa" do + test "returns 200 and disable 2fa", %{conn: conn} do + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: "otp_secret", confirmed: true} + } + ) + + response = + conn + |> put("/api/pleroma/admin/users/disable_mfa", %{nickname: user.nickname}) + |> json_response(200) + + assert response == user.nickname + mfa_settings = refresh_record(user).multi_factor_authentication_settings + + refute mfa_settings.enabled + refute mfa_settings.totp.confirmed + end + + test "returns 404 if user not found", %{conn: conn} do + response = + conn + |> put("/api/pleroma/admin/users/disable_mfa", %{nickname: "nickname"}) + |> json_response(404) + + assert response == "Not found" + end + end + describe "POST /api/pleroma/admin/users/invite_token" do test "without options", %{conn: conn} do conn = post(conn, "/api/pleroma/admin/users/invite_token") @@ -1620,6 +1697,25 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do end end + describe "GET /api/pleroma/admin/statuses/:id" do + test "not found", %{conn: conn} do + assert conn + |> get("/api/pleroma/admin/statuses/not_found") + |> json_response(:not_found) + end + + test "shows activity", %{conn: conn} do + activity = insert(:note_activity) + + response = + conn + |> get("/api/pleroma/admin/statuses/#{activity.id}") + |> json_response(200) + + assert response["id"] == activity.id + end + end + describe "PUT /api/pleroma/admin/statuses/:id" do setup do activity = insert(:note_activity) @@ -3526,7 +3622,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do end test "success", %{conn: conn} do - base_url = Pleroma.Web.base_url() + base_url = Web.base_url() app_name = "Trusted app" response = @@ -3547,7 +3643,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do end test "with trusted", %{conn: conn} do - base_url = Pleroma.Web.base_url() + base_url = Web.base_url() app_name = "Trusted app" response = diff --git a/test/web/auth/pleroma_authenticator_test.exs b/test/web/auth/pleroma_authenticator_test.exs new file mode 100644 index 000000000..7125c5081 --- /dev/null +++ b/test/web/auth/pleroma_authenticator_test.exs @@ -0,0 +1,43 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Auth.PleromaAuthenticatorTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.Auth.PleromaAuthenticator + import Pleroma.Factory + + setup do + password = "testpassword" + name = "AgentSmith" + user = insert(:user, nickname: name, password_hash: Comeonin.Pbkdf2.hashpwsalt(password)) + {:ok, [user: user, name: name, password: password]} + end + + test "get_user/authorization", %{user: user, name: name, password: password} do + params = %{"authorization" => %{"name" => name, "password" => password}} + res = PleromaAuthenticator.get_user(%Plug.Conn{params: params}) + + assert {:ok, user} == res + end + + test "get_user/authorization with invalid password", %{name: name} do + params = %{"authorization" => %{"name" => name, "password" => "password"}} + res = PleromaAuthenticator.get_user(%Plug.Conn{params: params}) + + assert {:error, {:checkpw, false}} == res + end + + test "get_user/grant_type_password", %{user: user, name: name, password: password} do + params = %{"grant_type" => "password", "username" => name, "password" => password} + res = PleromaAuthenticator.get_user(%Plug.Conn{params: params}) + + assert {:ok, user} == res + end + + test "error credintails" do + res = PleromaAuthenticator.get_user(%Plug.Conn{params: %{}}) + assert {:error, :invalid_credentials} == res + end +end diff --git a/test/web/auth/totp_authenticator_test.exs b/test/web/auth/totp_authenticator_test.exs new file mode 100644 index 000000000..e08069490 --- /dev/null +++ b/test/web/auth/totp_authenticator_test.exs @@ -0,0 +1,51 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Auth.TOTPAuthenticatorTest do + use Pleroma.Web.ConnCase + + alias Pleroma.MFA + alias Pleroma.MFA.BackupCodes + alias Pleroma.MFA.TOTP + alias Pleroma.Web.Auth.TOTPAuthenticator + + import Pleroma.Factory + + test "verify token" do + otp_secret = TOTP.generate_secret() + otp_token = TOTP.generate_token(otp_secret) + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + assert TOTPAuthenticator.verify(otp_token, user) == {:ok, :pass} + assert TOTPAuthenticator.verify(nil, user) == {:error, :invalid_token} + assert TOTPAuthenticator.verify("", user) == {:error, :invalid_token} + end + + test "checks backup codes" do + [code | _] = backup_codes = BackupCodes.generate() + + hashed_codes = + backup_codes + |> Enum.map(&Comeonin.Pbkdf2.hashpwsalt(&1)) + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + backup_codes: hashed_codes, + totp: %MFA.Settings.TOTP{secret: "otp_secret", confirmed: true} + } + ) + + assert TOTPAuthenticator.verify_recovery_code(user, code) == {:ok, :pass} + refute TOTPAuthenticator.verify_recovery_code(code, refresh_record(user)) == {:ok, :pass} + end +end diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index c6ccc02c4..2fd17a1b8 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -9,11 +9,13 @@ defmodule Pleroma.Web.CommonAPITest do alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.CommonAPI import Pleroma.Factory + import Mock require Pleroma.Constants @@ -21,6 +23,84 @@ defmodule Pleroma.Web.CommonAPITest do setup do: clear_config([:instance, :limit]) setup do: clear_config([:instance, :max_pinned_statuses]) + describe "deletion" do + test "it allows users to delete their posts" do + user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{"status" => "namu amida butsu"}) + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + assert {:ok, delete} = CommonAPI.delete(post.id, user) + assert delete.local + assert called(Pleroma.Web.Federator.publish(delete)) + end + + refute Activity.get_by_id(post.id) + end + + test "it does not allow a user to delete their posts" do + user = insert(:user) + other_user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{"status" => "namu amida butsu"}) + + assert {:error, "Could not delete"} = CommonAPI.delete(post.id, other_user) + assert Activity.get_by_id(post.id) + end + + test "it allows moderators to delete other user's posts" do + user = insert(:user) + moderator = insert(:user, is_moderator: true) + + {:ok, post} = CommonAPI.post(user, %{"status" => "namu amida butsu"}) + + assert {:ok, delete} = CommonAPI.delete(post.id, moderator) + assert delete.local + + refute Activity.get_by_id(post.id) + end + + test "it allows admins to delete other user's posts" do + user = insert(:user) + moderator = insert(:user, is_admin: true) + + {:ok, post} = CommonAPI.post(user, %{"status" => "namu amida butsu"}) + + assert {:ok, delete} = CommonAPI.delete(post.id, moderator) + assert delete.local + + refute Activity.get_by_id(post.id) + end + + test "superusers deleting non-local posts won't federate the delete" do + # This is the user of the ingested activity + _user = + insert(:user, + local: false, + ap_id: "http://mastodon.example.org/users/admin", + last_refreshed_at: NaiveDateTime.utc_now() + ) + + moderator = insert(:user, is_admin: true) + + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Jason.decode!() + + {:ok, post} = Transmogrifier.handle_incoming(data) + + with_mock Pleroma.Web.Federator, + publish: fn _ -> nil end do + assert {:ok, delete} = CommonAPI.delete(post.id, moderator) + assert delete.local + refute called(Pleroma.Web.Federator.publish(:_)) + end + + refute Activity.get_by_id(post.id) + end + end + test "favoriting race condition" do user = insert(:user) users_serial = insert_list(10, :user) @@ -278,7 +358,7 @@ defmodule Pleroma.Web.CommonAPITest do {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"}) - {:ok, reaction, _} = CommonAPI.react_with_emoji(activity.id, user, "👍") + {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍") assert reaction.data["actor"] == user.ap_id assert reaction.data["content"] == "👍" @@ -293,12 +373,13 @@ defmodule Pleroma.Web.CommonAPITest do other_user = insert(:user) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"}) - {:ok, reaction, _} = CommonAPI.react_with_emoji(activity.id, user, "👍") + {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍") - {:ok, unreaction, _} = CommonAPI.unreact_with_emoji(activity.id, user, "👍") + {:ok, unreaction} = CommonAPI.unreact_with_emoji(activity.id, user, "👍") assert unreaction.data["type"] == "Undo" assert unreaction.data["object"] == reaction.data["id"] + assert unreaction.local end test "repeating a status" do @@ -697,6 +778,14 @@ defmodule Pleroma.Web.CommonAPITest do assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject" assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending" end + + test "doesn't create a following relationship if the corresponding follow request doesn't exist" do + user = insert(:user, locked: true) + not_follower = insert(:user) + CommonAPI.accept_follow_request(not_follower, user) + + assert Pleroma.FollowingRelationship.following?(not_follower, user) == false + end end describe "vote/3" do diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index b9da7e924..256a8b304 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -1196,12 +1196,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do describe "verify_credentials" do test "verify_credentials" do %{user: user, conn: conn} = oauth_access(["read:accounts"]) + [notification | _] = insert_list(7, :notification, user: user) + Pleroma.Notification.set_read_up_to(user, notification.id) conn = get(conn, "/api/v1/accounts/verify_credentials") response = json_response_and_validate_schema(conn, 200) assert %{"id" => id, "source" => %{"privacy" => "public"}} = response assert response["pleroma"]["chat_token"] + assert response["pleroma"]["unread_notifications_count"] == 6 assert id == to_string(user.id) end diff --git a/test/web/mastodon_api/controllers/conversation_controller_test.exs b/test/web/mastodon_api/controllers/conversation_controller_test.exs index 801b0259b..04695572e 100644 --- a/test/web/mastodon_api/controllers/conversation_controller_test.exs +++ b/test/web/mastodon_api/controllers/conversation_controller_test.exs @@ -36,7 +36,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do res_conn = get(conn, "/api/v1/conversations") - assert response = json_response(res_conn, 200) + assert response = json_response_and_validate_schema(res_conn, 200) assert [ %{ @@ -91,18 +91,18 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do "visibility" => "direct" }) - [conversation1, conversation2] = - conn - |> get("/api/v1/conversations", %{"recipients" => [user_two.id]}) - |> json_response(200) + assert [conversation1, conversation2] = + conn + |> get("/api/v1/conversations?recipients[]=#{user_two.id}") + |> json_response_and_validate_schema(200) assert conversation1["last_status"]["id"] == direct5.id assert conversation2["last_status"]["id"] == direct1.id [conversation1] = conn - |> get("/api/v1/conversations", %{"recipients" => [user_two.id, user_three.id]}) - |> json_response(200) + |> get("/api/v1/conversations?recipients[]=#{user_two.id}&recipients[]=#{user_three.id}") + |> json_response_and_validate_schema(200) assert conversation1["last_status"]["id"] == direct3.id end @@ -126,7 +126,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do [%{"last_status" => res_last_status}] = conn |> get("/api/v1/conversations") - |> json_response(200) + |> json_response_and_validate_schema(200) assert res_last_status["id"] == direct_reply.id end @@ -154,12 +154,12 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do [%{"id" => direct_conversation_id, "unread" => true}] = user_two_conn |> get("/api/v1/conversations") - |> json_response(200) + |> json_response_and_validate_schema(200) %{"unread" => false} = user_two_conn |> post("/api/v1/conversations/#{direct_conversation_id}/read") - |> json_response(200) + |> json_response_and_validate_schema(200) assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 @@ -175,7 +175,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do [%{"unread" => true}] = conn |> get("/api/v1/conversations") - |> json_response(200) + |> json_response_and_validate_schema(200) assert User.get_cached_by_id(user_one.id).unread_conversation_count == 1 assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 diff --git a/test/web/mastodon_api/controllers/filter_controller_test.exs b/test/web/mastodon_api/controllers/filter_controller_test.exs index 97ab005e0..f29547d13 100644 --- a/test/web/mastodon_api/controllers/filter_controller_test.exs +++ b/test/web/mastodon_api/controllers/filter_controller_test.exs @@ -15,9 +15,12 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do context: ["home"] } - conn = post(conn, "/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context}) + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context}) - assert response = json_response(conn, 200) + assert response = json_response_and_validate_schema(conn, 200) assert response["phrase"] == filter.phrase assert response["context"] == filter.context assert response["irreversible"] == false @@ -48,12 +51,12 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do response = conn |> get("/api/v1/filters") - |> json_response(200) + |> json_response_and_validate_schema(200) assert response == render_json( FilterView, - "filters.json", + "index.json", filters: [filter_two, filter_one] ) end @@ -72,7 +75,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do conn = get(conn, "/api/v1/filters/#{filter.filter_id}") - assert _response = json_response(conn, 200) + assert response = json_response_and_validate_schema(conn, 200) end test "update a filter" do @@ -82,7 +85,8 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do user_id: user.id, filter_id: 2, phrase: "knight", - context: ["home"] + context: ["home"], + hide: true } {:ok, _filter} = Pleroma.Filter.create(query) @@ -93,14 +97,17 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do } conn = - put(conn, "/api/v1/filters/#{query.filter_id}", %{ + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/filters/#{query.filter_id}", %{ phrase: new.phrase, context: new.context }) - assert response = json_response(conn, 200) + assert response = json_response_and_validate_schema(conn, 200) assert response["phrase"] == new.phrase assert response["context"] == new.context + assert response["irreversible"] == true end test "delete a filter" do @@ -117,7 +124,6 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do conn = delete(conn, "/api/v1/filters/#{filter.filter_id}") - assert response = json_response(conn, 200) - assert response == %{} + assert json_response_and_validate_schema(conn, 200) == %{} end end diff --git a/test/web/mastodon_api/controllers/follow_request_controller_test.exs b/test/web/mastodon_api/controllers/follow_request_controller_test.exs index d8dbe4800..44e12d15a 100644 --- a/test/web/mastodon_api/controllers/follow_request_controller_test.exs +++ b/test/web/mastodon_api/controllers/follow_request_controller_test.exs @@ -27,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do conn = get(conn, "/api/v1/follow_requests") - assert [relationship] = json_response(conn, 200) + assert [relationship] = json_response_and_validate_schema(conn, 200) assert to_string(other_user.id) == relationship["id"] end @@ -44,7 +44,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do conn = post(conn, "/api/v1/follow_requests/#{other_user.id}/authorize") - assert relationship = json_response(conn, 200) + assert relationship = json_response_and_validate_schema(conn, 200) assert to_string(other_user.id) == relationship["id"] user = User.get_cached_by_id(user.id) @@ -62,7 +62,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do conn = post(conn, "/api/v1/follow_requests/#{other_user.id}/reject") - assert relationship = json_response(conn, 200) + assert relationship = json_response_and_validate_schema(conn, 200) assert to_string(other_user.id) == relationship["id"] user = User.get_cached_by_id(user.id) diff --git a/test/web/mastodon_api/controllers/instance_controller_test.exs b/test/web/mastodon_api/controllers/instance_controller_test.exs index 2737dcaba..90840d5ab 100644 --- a/test/web/mastodon_api/controllers/instance_controller_test.exs +++ b/test/web/mastodon_api/controllers/instance_controller_test.exs @@ -10,7 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do test "get instance information", %{conn: conn} do conn = get(conn, "/api/v1/instance") - assert result = json_response(conn, 200) + assert result = json_response_and_validate_schema(conn, 200) email = Pleroma.Config.get([:instance, :email]) # Note: not checking for "max_toot_chars" since it's optional @@ -34,6 +34,10 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do "banner_upload_limit" => _ } = result + assert result["pleroma"]["metadata"]["features"] + assert result["pleroma"]["metadata"]["federation"] + assert result["pleroma"]["vapid_public_key"] + assert email == from_config_email end @@ -52,7 +56,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do conn = get(conn, "/api/v1/instance") - assert result = json_response(conn, 200) + assert result = json_response_and_validate_schema(conn, 200) stats = result["stats"] @@ -70,7 +74,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do conn = get(conn, "/api/v1/instance/peers") - assert result = json_response(conn, 200) + assert result = json_response_and_validate_schema(conn, 200) assert ["peer1.com", "peer2.com"] == Enum.sort(result) end diff --git a/test/web/mastodon_api/controllers/list_controller_test.exs b/test/web/mastodon_api/controllers/list_controller_test.exs index c9c4cbb49..57a9ef4a4 100644 --- a/test/web/mastodon_api/controllers/list_controller_test.exs +++ b/test/web/mastodon_api/controllers/list_controller_test.exs @@ -12,37 +12,44 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do test "creating a list" do %{conn: conn} = oauth_access(["write:lists"]) - conn = post(conn, "/api/v1/lists", %{"title" => "cuties"}) - - assert %{"title" => title} = json_response(conn, 200) - assert title == "cuties" + assert %{"title" => "cuties"} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/lists", %{"title" => "cuties"}) + |> json_response_and_validate_schema(:ok) end test "renders error for invalid params" do %{conn: conn} = oauth_access(["write:lists"]) - conn = post(conn, "/api/v1/lists", %{"title" => nil}) + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/lists", %{"title" => nil}) - assert %{"error" => "can't be blank"} == json_response(conn, :unprocessable_entity) + assert %{"error" => "title - null value where string expected."} = + json_response_and_validate_schema(conn, 400) end test "listing a user's lists" do %{conn: conn} = oauth_access(["read:lists", "write:lists"]) conn + |> put_req_header("content-type", "application/json") |> post("/api/v1/lists", %{"title" => "cuties"}) - |> json_response(:ok) + |> json_response_and_validate_schema(:ok) conn + |> put_req_header("content-type", "application/json") |> post("/api/v1/lists", %{"title" => "cofe"}) - |> json_response(:ok) + |> json_response_and_validate_schema(:ok) conn = get(conn, "/api/v1/lists") assert [ %{"id" => _, "title" => "cofe"}, %{"id" => _, "title" => "cuties"} - ] = json_response(conn, :ok) + ] = json_response_and_validate_schema(conn, :ok) end test "adding users to a list" do @@ -50,9 +57,12 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do other_user = insert(:user) {:ok, list} = Pleroma.List.create("name", user) - conn = post(conn, "/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) + assert %{} == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) + |> json_response_and_validate_schema(:ok) - assert %{} == json_response(conn, 200) %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) assert following == [other_user.follower_address] end @@ -65,9 +75,12 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do {:ok, list} = Pleroma.List.follow(list, other_user) {:ok, list} = Pleroma.List.follow(list, third_user) - conn = delete(conn, "/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) + assert %{} == + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) + |> json_response_and_validate_schema(:ok) - assert %{} == json_response(conn, 200) %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) assert following == [third_user.follower_address] end @@ -83,7 +96,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do |> assign(:user, user) |> get("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) - assert [%{"id" => id}] = json_response(conn, 200) + assert [%{"id" => id}] = json_response_and_validate_schema(conn, 200) assert id == to_string(other_user.id) end @@ -96,7 +109,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do |> assign(:user, user) |> get("/api/v1/lists/#{list.id}") - assert %{"id" => id} = json_response(conn, 200) + assert %{"id" => id} = json_response_and_validate_schema(conn, 200) assert id == to_string(list.id) end @@ -105,17 +118,18 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do conn = get(conn, "/api/v1/lists/666") - assert %{"error" => "List not found"} = json_response(conn, :not_found) + assert %{"error" => "List not found"} = json_response_and_validate_schema(conn, :not_found) end test "renaming a list" do %{user: user, conn: conn} = oauth_access(["write:lists"]) {:ok, list} = Pleroma.List.create("name", user) - conn = put(conn, "/api/v1/lists/#{list.id}", %{"title" => "newname"}) - - assert %{"title" => name} = json_response(conn, 200) - assert name == "newname" + assert %{"title" => "newname"} = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/lists/#{list.id}", %{"title" => "newname"}) + |> json_response_and_validate_schema(:ok) end test "validates title when renaming a list" do @@ -125,9 +139,11 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do conn = conn |> assign(:user, user) + |> put_req_header("content-type", "application/json") |> put("/api/v1/lists/#{list.id}", %{"title" => " "}) - assert %{"error" => "can't be blank"} == json_response(conn, :unprocessable_entity) + assert %{"error" => "can't be blank"} == + json_response_and_validate_schema(conn, :unprocessable_entity) end test "deleting a list" do @@ -136,7 +152,7 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do conn = delete(conn, "/api/v1/lists/#{list.id}") - assert %{} = json_response(conn, 200) + assert %{} = json_response_and_validate_schema(conn, 200) assert is_nil(Repo.get(Pleroma.List, list.id)) end end diff --git a/test/web/mastodon_api/controllers/marker_controller_test.exs b/test/web/mastodon_api/controllers/marker_controller_test.exs index 919f295bd..6dd40fb4a 100644 --- a/test/web/mastodon_api/controllers/marker_controller_test.exs +++ b/test/web/mastodon_api/controllers/marker_controller_test.exs @@ -11,6 +11,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do test "gets markers with correct scopes", %{conn: conn} do user = insert(:user) token = insert(:oauth_token, user: user, scopes: ["read:statuses"]) + insert_list(7, :notification, user: user) {:ok, %{"notifications" => marker}} = Pleroma.Marker.upsert( @@ -22,14 +23,15 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do conn |> assign(:user, user) |> assign(:token, token) - |> get("/api/v1/markers", %{timeline: ["notifications"]}) - |> json_response(200) + |> get("/api/v1/markers?timeline[]=notifications") + |> json_response_and_validate_schema(200) assert response == %{ "notifications" => %{ "last_read_id" => "69420", "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at), - "version" => 0 + "version" => 0, + "pleroma" => %{"unread_count" => 7} } } end @@ -45,7 +47,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do |> assign(:user, user) |> assign(:token, token) |> get("/api/v1/markers", %{timeline: ["notifications"]}) - |> json_response(403) + |> json_response_and_validate_schema(403) assert response == %{"error" => "Insufficient permissions: read:statuses."} end @@ -60,17 +62,19 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do conn |> assign(:user, user) |> assign(:token, token) + |> put_req_header("content-type", "application/json") |> post("/api/v1/markers", %{ home: %{last_read_id: "777"}, notifications: %{"last_read_id" => "69420"} }) - |> json_response(200) + |> json_response_and_validate_schema(200) assert %{ "notifications" => %{ "last_read_id" => "69420", "updated_at" => _, - "version" => 0 + "version" => 0, + "pleroma" => %{"unread_count" => 0} } } = response end @@ -89,17 +93,19 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do conn |> assign(:user, user) |> assign(:token, token) + |> put_req_header("content-type", "application/json") |> post("/api/v1/markers", %{ home: %{last_read_id: "777"}, notifications: %{"last_read_id" => "69888"} }) - |> json_response(200) + |> json_response_and_validate_schema(200) assert response == %{ "notifications" => %{ "last_read_id" => "69888", "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at), - "version" => 0 + "version" => 0, + "pleroma" => %{"unread_count" => 0} } } end @@ -112,11 +118,12 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do conn |> assign(:user, user) |> assign(:token, token) + |> put_req_header("content-type", "application/json") |> post("/api/v1/markers", %{ home: %{last_read_id: "777"}, notifications: %{"last_read_id" => "69420"} }) - |> json_response(403) + |> json_response_and_validate_schema(403) assert response == %{"error" => "Insufficient permissions: write:statuses."} end diff --git a/test/web/mastodon_api/controllers/poll_controller_test.exs b/test/web/mastodon_api/controllers/poll_controller_test.exs index 88b13a25a..d8f34aa86 100644 --- a/test/web/mastodon_api/controllers/poll_controller_test.exs +++ b/test/web/mastodon_api/controllers/poll_controller_test.exs @@ -24,7 +24,7 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do conn = get(conn, "/api/v1/polls/#{object.id}") - response = json_response(conn, 200) + response = json_response_and_validate_schema(conn, 200) id = to_string(object.id) assert %{"id" => ^id, "expired" => false, "multiple" => false} = response end @@ -43,7 +43,7 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do conn = get(conn, "/api/v1/polls/#{object.id}") - assert json_response(conn, 404) + assert json_response_and_validate_schema(conn, 404) end end @@ -65,9 +65,12 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do object = Object.normalize(activity) - conn = post(conn, "/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]}) + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]}) - assert json_response(conn, 200) + assert json_response_and_validate_schema(conn, 200) object = Object.get_by_id(object.id) assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => total_items}} -> @@ -85,8 +88,9 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do object = Object.normalize(activity) assert conn + |> put_req_header("content-type", "application/json") |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]}) - |> json_response(422) == %{"error" => "Poll's author can't vote"} + |> json_response_and_validate_schema(422) == %{"error" => "Poll's author can't vote"} object = Object.get_by_id(object.id) @@ -105,8 +109,9 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do object = Object.normalize(activity) assert conn + |> put_req_header("content-type", "application/json") |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1]}) - |> json_response(422) == %{"error" => "Too many choices"} + |> json_response_and_validate_schema(422) == %{"error" => "Too many choices"} object = Object.get_by_id(object.id) @@ -126,15 +131,21 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do object = Object.normalize(activity) - conn = post(conn, "/api/v1/polls/#{object.id}/votes", %{"choices" => [2]}) + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [2]}) - assert json_response(conn, 422) == %{"error" => "Invalid indices"} + assert json_response_and_validate_schema(conn, 422) == %{"error" => "Invalid indices"} end test "returns 404 error when object is not exist", %{conn: conn} do - conn = post(conn, "/api/v1/polls/1/votes", %{"choices" => [0]}) + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/polls/1/votes", %{"choices" => [0]}) - assert json_response(conn, 404) == %{"error" => "Record not found"} + assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} end test "returns 404 when poll is private and not available for user", %{conn: conn} do @@ -149,9 +160,12 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do object = Object.normalize(activity) - conn = post(conn, "/api/v1/polls/#{object.id}/votes", %{"choices" => [0]}) + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0]}) - assert json_response(conn, 404) == %{"error" => "Record not found"} + assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} end end end diff --git a/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs b/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs index f86274d57..1ff871c89 100644 --- a/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs +++ b/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs @@ -24,19 +24,19 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do # min_id conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&min_id=#{scheduled_activity_id1}") - result = json_response(conn_res, 200) + result = json_response_and_validate_schema(conn_res, 200) assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result # since_id conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&since_id=#{scheduled_activity_id1}") - result = json_response(conn_res, 200) + result = json_response_and_validate_schema(conn_res, 200) assert [%{"id" => ^scheduled_activity_id4}, %{"id" => ^scheduled_activity_id3}] = result # max_id conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&max_id=#{scheduled_activity_id4}") - result = json_response(conn_res, 200) + result = json_response_and_validate_schema(conn_res, 200) assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result end @@ -46,12 +46,12 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do res_conn = get(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}") - assert %{"id" => scheduled_activity_id} = json_response(res_conn, 200) + assert %{"id" => scheduled_activity_id} = json_response_and_validate_schema(res_conn, 200) assert scheduled_activity_id == scheduled_activity.id |> to_string() res_conn = get(conn, "/api/v1/scheduled_statuses/404") - assert %{"error" => "Record not found"} = json_response(res_conn, 404) + assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404) end test "updates a scheduled activity" do @@ -74,22 +74,32 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do assert job.args == %{"activity_id" => scheduled_activity.id} assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(scheduled_at) - new_scheduled_at = Timex.shift(NaiveDateTime.utc_now(), minutes: 120) + new_scheduled_at = + NaiveDateTime.utc_now() + |> Timex.shift(minutes: 120) + |> Timex.format!("%Y-%m-%dT%H:%M:%S.%fZ", :strftime) res_conn = - put(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{ + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{ scheduled_at: new_scheduled_at }) - assert %{"scheduled_at" => expected_scheduled_at} = json_response(res_conn, 200) + assert %{"scheduled_at" => expected_scheduled_at} = + json_response_and_validate_schema(res_conn, 200) + assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(new_scheduled_at) job = refresh_record(job) assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(new_scheduled_at) - res_conn = put(conn, "/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at}) + res_conn = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at}) - assert %{"error" => "Record not found"} = json_response(res_conn, 404) + assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404) end test "deletes a scheduled activity" do @@ -115,7 +125,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do |> assign(:user, user) |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}") - assert %{} = json_response(res_conn, 200) + assert %{} = json_response_and_validate_schema(res_conn, 200) refute Repo.get(ScheduledActivity, scheduled_activity.id) refute Repo.get(Oban.Job, job.id) @@ -124,6 +134,6 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do |> assign(:user, user) |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}") - assert %{"error" => "Record not found"} = json_response(res_conn, 404) + assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404) end end diff --git a/test/web/mastodon_api/controllers/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs index 11133ff66..02476acb6 100644 --- a/test/web/mastodon_api/controllers/search_controller_test.exs +++ b/test/web/mastodon_api/controllers/search_controller_test.exs @@ -27,8 +27,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do capture_log(fn -> results = conn - |> get("/api/v2/search", %{"q" => "2hu"}) - |> json_response(200) + |> get("/api/v2/search?q=2hu") + |> json_response_and_validate_schema(200) assert results["accounts"] == [] assert results["statuses"] == [] @@ -54,8 +54,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do results = conn - |> get("/api/v2/search", %{"q" => "2hu #private"}) - |> json_response(200) + |> get("/api/v2/search?#{URI.encode_query(%{q: "2hu #private"})}") + |> json_response_and_validate_schema(200) [account | _] = results["accounts"] assert account["id"] == to_string(user_three.id) @@ -68,8 +68,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do assert status["id"] == to_string(activity.id) results = - get(conn, "/api/v2/search", %{"q" => "天子"}) - |> json_response(200) + get(conn, "/api/v2/search?q=天子") + |> json_response_and_validate_schema(200) [status] = results["statuses"] assert status["id"] == to_string(activity.id) @@ -89,8 +89,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do conn |> assign(:user, user) |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"])) - |> get("/api/v2/search", %{"q" => "Agent"}) - |> json_response(200) + |> get("/api/v2/search?q=Agent") + |> json_response_and_validate_schema(200) status_ids = Enum.map(results["statuses"], fn g -> g["id"] end) @@ -107,8 +107,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do results = conn - |> get("/api/v1/accounts/search", %{"q" => "shp"}) - |> json_response(200) + |> get("/api/v1/accounts/search?q=shp") + |> json_response_and_validate_schema(200) result_ids = for result <- results, do: result["acct"] @@ -117,8 +117,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do results = conn - |> get("/api/v1/accounts/search", %{"q" => "2hu"}) - |> json_response(200) + |> get("/api/v1/accounts/search?q=2hu") + |> json_response_and_validate_schema(200) result_ids = for result <- results, do: result["acct"] @@ -130,8 +130,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do results = conn - |> get("/api/v1/accounts/search", %{"q" => "shp@shitposter.club xxx "}) - |> json_response(200) + |> get("/api/v1/accounts/search?q=shp@shitposter.club xxx") + |> json_response_and_validate_schema(200) assert length(results) == 1 end @@ -146,8 +146,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do capture_log(fn -> results = conn - |> get("/api/v1/search", %{"q" => "2hu"}) - |> json_response(200) + |> get("/api/v1/search?q=2hu") + |> json_response_and_validate_schema(200) assert results["accounts"] == [] assert results["statuses"] == [] @@ -173,8 +173,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do results = conn - |> get("/api/v1/search", %{"q" => "2hu"}) - |> json_response(200) + |> get("/api/v1/search?q=2hu") + |> json_response_and_validate_schema(200) [account | _] = results["accounts"] assert account["id"] == to_string(user_three.id) @@ -194,8 +194,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do results = conn - |> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"}) - |> json_response(200) + |> get("/api/v1/search?q=https://shitposter.club/notice/2827873") + |> json_response_and_validate_schema(200) [status, %{"id" => ^activity_id}] = results["statuses"] @@ -212,10 +212,12 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do }) capture_log(fn -> + q = Object.normalize(activity).data["id"] + results = conn - |> get("/api/v1/search", %{"q" => Object.normalize(activity).data["id"]}) - |> json_response(200) + |> get("/api/v1/search?q=#{q}") + |> json_response_and_validate_schema(200) [] = results["statuses"] end) @@ -228,8 +230,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do conn |> assign(:user, user) |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"])) - |> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "true"}) - |> json_response(200) + |> get("/api/v1/search?q=mike@osada.macgirvin.com&resolve=true") + |> json_response_and_validate_schema(200) [account] = results["accounts"] assert account["acct"] == "mike@osada.macgirvin.com" @@ -238,8 +240,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do results = conn - |> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "false"}) - |> json_response(200) + |> get("/api/v1/search?q=mike@osada.macgirvin.com&resolve=false") + |> json_response_and_validate_schema(200) assert [] == results["accounts"] end @@ -254,16 +256,16 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do result = conn - |> get("/api/v1/search", %{"q" => "2hu", "limit" => 1}) + |> get("/api/v1/search?q=2hu&limit=1") - assert results = json_response(result, 200) + assert results = json_response_and_validate_schema(result, 200) assert [%{"id" => activity_id1}] = results["statuses"] assert [_] = results["accounts"] results = conn - |> get("/api/v1/search", %{"q" => "2hu", "limit" => 1, "offset" => 1}) - |> json_response(200) + |> get("/api/v1/search?q=2hu&limit=1&offset=1") + |> json_response_and_validate_schema(200) assert [%{"id" => activity_id2}] = results["statuses"] assert [] = results["accounts"] @@ -279,13 +281,13 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do assert %{"statuses" => [_activity], "accounts" => [], "hashtags" => []} = conn - |> get("/api/v1/search", %{"q" => "2hu", "type" => "statuses"}) - |> json_response(200) + |> get("/api/v1/search?q=2hu&type=statuses") + |> json_response_and_validate_schema(200) assert %{"statuses" => [], "accounts" => [_user_two], "hashtags" => []} = conn - |> get("/api/v1/search", %{"q" => "2hu", "type" => "accounts"}) - |> json_response(200) + |> get("/api/v1/search?q=2hu&type=accounts") + |> json_response_and_validate_schema(200) end test "search uses account_id to filter statuses by the author", %{conn: conn} do @@ -297,8 +299,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do results = conn - |> get("/api/v1/search", %{"q" => "2hu", "account_id" => user.id}) - |> json_response(200) + |> get("/api/v1/search?q=2hu&account_id=#{user.id}") + |> json_response_and_validate_schema(200) assert [%{"id" => activity_id1}] = results["statuses"] assert activity_id1 == activity1.id @@ -306,8 +308,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do results = conn - |> get("/api/v1/search", %{"q" => "2hu", "account_id" => user_two.id}) - |> json_response(200) + |> get("/api/v1/search?q=2hu&account_id=#{user_two.id}") + |> json_response_and_validate_schema(200) assert [%{"id" => activity_id2}] = results["statuses"] assert activity_id2 == activity2.id diff --git a/test/web/mastodon_api/controllers/subscription_controller_test.exs b/test/web/mastodon_api/controllers/subscription_controller_test.exs index 5682498c0..4aa260663 100644 --- a/test/web/mastodon_api/controllers/subscription_controller_test.exs +++ b/test/web/mastodon_api/controllers/subscription_controller_test.exs @@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do use Pleroma.Web.ConnCase import Pleroma.Factory + alias Pleroma.Web.Push alias Pleroma.Web.Push.Subscription @@ -27,6 +28,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do build_conn() |> assign(:user, user) |> assign(:token, token) + |> put_req_header("content-type", "application/json") %{conn: conn, user: user, token: token} end @@ -47,8 +49,8 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do test "returns error when push disabled ", %{conn: conn} do assert_error_when_disable_push do conn - |> post("/api/v1/push/subscription", %{}) - |> json_response(403) + |> post("/api/v1/push/subscription", %{subscription: @sub}) + |> json_response_and_validate_schema(403) end end @@ -59,7 +61,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do "data" => %{"alerts" => %{"mention" => true, "test" => true}}, "subscription" => @sub }) - |> json_response(200) + |> json_response_and_validate_schema(200) [subscription] = Pleroma.Repo.all(Subscription) @@ -77,7 +79,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do assert_error_when_disable_push do conn |> get("/api/v1/push/subscription", %{}) - |> json_response(403) + |> json_response_and_validate_schema(403) end end @@ -85,9 +87,9 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do res = conn |> get("/api/v1/push/subscription", %{}) - |> json_response(404) + |> json_response_and_validate_schema(404) - assert "Not found" == res + assert %{"error" => "Record not found"} == res end test "returns a user subsciption", %{conn: conn, user: user, token: token} do @@ -101,7 +103,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do res = conn |> get("/api/v1/push/subscription", %{}) - |> json_response(200) + |> json_response_and_validate_schema(200) expect = %{ "alerts" => %{"mention" => true}, @@ -130,7 +132,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do assert_error_when_disable_push do conn |> put("/api/v1/push/subscription", %{data: %{"alerts" => %{"mention" => false}}}) - |> json_response(403) + |> json_response_and_validate_schema(403) end end @@ -140,7 +142,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do |> put("/api/v1/push/subscription", %{ data: %{"alerts" => %{"mention" => false, "follow" => true}} }) - |> json_response(200) + |> json_response_and_validate_schema(200) expect = %{ "alerts" => %{"follow" => true, "mention" => false}, @@ -158,7 +160,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do assert_error_when_disable_push do conn |> delete("/api/v1/push/subscription", %{}) - |> json_response(403) + |> json_response_and_validate_schema(403) end end @@ -166,9 +168,9 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do res = conn |> delete("/api/v1/push/subscription", %{}) - |> json_response(404) + |> json_response_and_validate_schema(404) - assert "Not found" == res + assert %{"error" => "Record not found"} == res end test "returns empty result and delete user subsciption", %{ @@ -186,7 +188,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do res = conn |> delete("/api/v1/push/subscription", %{}) - |> json_response(200) + |> json_response_and_validate_schema(200) assert %{} == res refute Pleroma.Repo.get(Subscription, subscription.id) diff --git a/test/web/mastodon_api/controllers/suggestion_controller_test.exs b/test/web/mastodon_api/controllers/suggestion_controller_test.exs index 8d0e70db8..f120bd0cd 100644 --- a/test/web/mastodon_api/controllers/suggestion_controller_test.exs +++ b/test/web/mastodon_api/controllers/suggestion_controller_test.exs @@ -5,8 +5,6 @@ defmodule Pleroma.Web.MastodonAPI.SuggestionControllerTest do use Pleroma.Web.ConnCase - alias Pleroma.Config - setup do: oauth_access(["read"]) test "returns empty result", %{conn: conn} do diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index 85fa4f6a2..5fb162141 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -466,6 +466,24 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do :unread_conversation_count ] == 1 end + + test "shows unread_count only to the account owner" do + user = insert(:user) + insert_list(7, :notification, user: user) + other_user = insert(:user) + + user = User.get_cached_by_ap_id(user.ap_id) + + assert AccountView.render( + "show.json", + %{user: user, for: other_user} + )[:pleroma][:unread_notifications_count] == nil + + assert AccountView.render( + "show.json", + %{user: user, for: user} + )[:pleroma][:unread_notifications_count] == 7 + end end describe "follow requests counter" do diff --git a/test/web/mastodon_api/views/marker_view_test.exs b/test/web/mastodon_api/views/marker_view_test.exs index 893cf8857..48a0a6d33 100644 --- a/test/web/mastodon_api/views/marker_view_test.exs +++ b/test/web/mastodon_api/views/marker_view_test.exs @@ -8,19 +8,21 @@ defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do import Pleroma.Factory test "returns markers" do - marker1 = insert(:marker, timeline: "notifications", last_read_id: "17") + marker1 = insert(:marker, timeline: "notifications", last_read_id: "17", unread_count: 5) marker2 = insert(:marker, timeline: "home", last_read_id: "42") assert MarkerView.render("markers.json", %{markers: [marker1, marker2]}) == %{ "home" => %{ last_read_id: "42", updated_at: NaiveDateTime.to_iso8601(marker2.updated_at), - version: 0 + version: 0, + pleroma: %{unread_count: 0} }, "notifications" => %{ last_read_id: "17", updated_at: NaiveDateTime.to_iso8601(marker1.updated_at), - version: 0 + version: 0, + pleroma: %{unread_count: 5} } } end diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs index e1f9c3ac4..61e6e3ae5 100644 --- a/test/web/mastodon_api/views/notification_view_test.exs +++ b/test/web/mastodon_api/views/notification_view_test.exs @@ -166,7 +166,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do other_user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"}) - {:ok, _activity, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + {:ok, _activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") activity = Repo.get(Activity, activity.id) diff --git a/test/web/mastodon_api/views/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs index 91d4ded2c..d46f5c396 100644 --- a/test/web/mastodon_api/views/status_view_test.exs +++ b/test/web/mastodon_api/views/status_view_test.exs @@ -32,9 +32,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do third_user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{"status" => "dae cofe??"}) - {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "☕") - {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵") - {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "☕") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") activity = Repo.get(Activity, activity.id) status = StatusView.render("show.json", activity: activity) @@ -402,11 +402,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do pleroma: %{mime_type: "image/png"} } + api_spec = Pleroma.Web.ApiSpec.spec() + assert expected == StatusView.render("attachment.json", %{attachment: object}) + OpenApiSpex.TestAssertions.assert_schema(expected, "Attachment", api_spec) # If theres a "id", use that instead of the generated one object = Map.put(object, "id", 2) - assert %{id: "2"} = StatusView.render("attachment.json", %{attachment: object}) + result = StatusView.render("attachment.json", %{attachment: object}) + + assert %{id: "2"} = result + OpenApiSpex.TestAssertions.assert_schema(result, "Attachment", api_spec) end test "put the url advertised in the Activity in to the url attribute" do diff --git a/test/web/oauth/mfa_controller_test.exs b/test/web/oauth/mfa_controller_test.exs new file mode 100644 index 000000000..ce4a07320 --- /dev/null +++ b/test/web/oauth/mfa_controller_test.exs @@ -0,0 +1,306 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.MFAControllerTest do + use Pleroma.Web.ConnCase + import Pleroma.Factory + + alias Pleroma.MFA + alias Pleroma.MFA.BackupCodes + alias Pleroma.MFA.TOTP + alias Pleroma.Repo + alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.OAuthController + + setup %{conn: conn} do + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + backup_codes: [Comeonin.Pbkdf2.hashpwsalt("test-code")], + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + app = insert(:oauth_app) + {:ok, conn: conn, user: user, app: app} + end + + describe "show" do + setup %{conn: conn, user: user, app: app} do + mfa_token = + insert(:mfa_token, + user: user, + authorization: build(:oauth_authorization, app: app, scopes: ["write"]) + ) + + {:ok, conn: conn, mfa_token: mfa_token} + end + + test "GET /oauth/mfa renders mfa forms", %{conn: conn, mfa_token: mfa_token} do + conn = + get( + conn, + "/oauth/mfa", + %{ + "mfa_token" => mfa_token.token, + "state" => "a_state", + "redirect_uri" => "http://localhost:8080/callback" + } + ) + + assert response = html_response(conn, 200) + assert response =~ "Two-factor authentication" + assert response =~ mfa_token.token + assert response =~ "http://localhost:8080/callback" + end + + test "GET /oauth/mfa renders mfa recovery forms", %{conn: conn, mfa_token: mfa_token} do + conn = + get( + conn, + "/oauth/mfa", + %{ + "mfa_token" => mfa_token.token, + "state" => "a_state", + "redirect_uri" => "http://localhost:8080/callback", + "challenge_type" => "recovery" + } + ) + + assert response = html_response(conn, 200) + assert response =~ "Two-factor recovery" + assert response =~ mfa_token.token + assert response =~ "http://localhost:8080/callback" + end + end + + describe "verify" do + setup %{conn: conn, user: user, app: app} do + mfa_token = + insert(:mfa_token, + user: user, + authorization: build(:oauth_authorization, app: app, scopes: ["write"]) + ) + + {:ok, conn: conn, user: user, mfa_token: mfa_token, app: app} + end + + test "POST /oauth/mfa/verify, verify totp code", %{ + conn: conn, + user: user, + mfa_token: mfa_token, + app: app + } do + otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret) + + conn = + conn + |> post("/oauth/mfa/verify", %{ + "mfa" => %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "totp", + "code" => otp_token, + "state" => "a_state", + "redirect_uri" => OAuthController.default_redirect_uri(app) + } + }) + + target = redirected_to(conn) + target_url = %URI{URI.parse(target) | query: nil} |> URI.to_string() + query = URI.parse(target).query |> URI.query_decoder() |> Map.new() + assert %{"state" => "a_state", "code" => code} = query + assert target_url == OAuthController.default_redirect_uri(app) + auth = Repo.get_by(Authorization, token: code) + assert auth.scopes == ["write"] + end + + test "POST /oauth/mfa/verify, verify recovery code", %{ + conn: conn, + mfa_token: mfa_token, + app: app + } do + conn = + conn + |> post("/oauth/mfa/verify", %{ + "mfa" => %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "recovery", + "code" => "test-code", + "state" => "a_state", + "redirect_uri" => OAuthController.default_redirect_uri(app) + } + }) + + target = redirected_to(conn) + target_url = %URI{URI.parse(target) | query: nil} |> URI.to_string() + query = URI.parse(target).query |> URI.query_decoder() |> Map.new() + assert %{"state" => "a_state", "code" => code} = query + assert target_url == OAuthController.default_redirect_uri(app) + auth = Repo.get_by(Authorization, token: code) + assert auth.scopes == ["write"] + end + end + + describe "challenge/totp" do + test "returns access token with valid code", %{conn: conn, user: user, app: app} do + otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret) + + mfa_token = + insert(:mfa_token, + user: user, + authorization: build(:oauth_authorization, app: app, scopes: ["write"]) + ) + + response = + conn + |> post("/oauth/mfa/challenge", %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "totp", + "code" => otp_token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(:ok) + + ap_id = user.ap_id + + assert match?( + %{ + "access_token" => _, + "expires_in" => 600, + "me" => ^ap_id, + "refresh_token" => _, + "scope" => "write", + "token_type" => "Bearer" + }, + response + ) + end + + test "returns errors when mfa token invalid", %{conn: conn, user: user, app: app} do + otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret) + + response = + conn + |> post("/oauth/mfa/challenge", %{ + "mfa_token" => "XXX", + "challenge_type" => "totp", + "code" => otp_token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(400) + + assert response == %{"error" => "Invalid code"} + end + + test "returns error when otp code is invalid", %{conn: conn, user: user, app: app} do + mfa_token = insert(:mfa_token, user: user) + + response = + conn + |> post("/oauth/mfa/challenge", %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "totp", + "code" => "XXX", + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(400) + + assert response == %{"error" => "Invalid code"} + end + + test "returns error when client credentails is wrong ", %{conn: conn, user: user} do + otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret) + mfa_token = insert(:mfa_token, user: user) + + response = + conn + |> post("/oauth/mfa/challenge", %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "totp", + "code" => otp_token, + "client_id" => "xxx", + "client_secret" => "xxx" + }) + |> json_response(400) + + assert response == %{"error" => "Invalid code"} + end + end + + describe "challenge/recovery" do + setup %{conn: conn} do + app = insert(:oauth_app) + {:ok, conn: conn, app: app} + end + + test "returns access token with valid code", %{conn: conn, app: app} do + otp_secret = TOTP.generate_secret() + + [code | _] = backup_codes = BackupCodes.generate() + + hashed_codes = + backup_codes + |> Enum.map(&Comeonin.Pbkdf2.hashpwsalt(&1)) + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + backup_codes: hashed_codes, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + mfa_token = + insert(:mfa_token, + user: user, + authorization: build(:oauth_authorization, app: app, scopes: ["write"]) + ) + + response = + conn + |> post("/oauth/mfa/challenge", %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "recovery", + "code" => code, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(:ok) + + ap_id = user.ap_id + + assert match?( + %{ + "access_token" => _, + "expires_in" => 600, + "me" => ^ap_id, + "refresh_token" => _, + "scope" => "write", + "token_type" => "Bearer" + }, + response + ) + + error_response = + conn + |> post("/oauth/mfa/challenge", %{ + "mfa_token" => mfa_token.token, + "challenge_type" => "recovery", + "code" => code, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(400) + + assert error_response == %{"error" => "Invalid code"} + end + end +end diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs index f2f98d768..7a107584d 100644 --- a/test/web/oauth/oauth_controller_test.exs +++ b/test/web/oauth/oauth_controller_test.exs @@ -6,6 +6,8 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do use Pleroma.Web.ConnCase import Pleroma.Factory + alias Pleroma.MFA + alias Pleroma.MFA.TOTP alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.OAuth.Authorization @@ -604,6 +606,41 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do end end + test "redirect to on two-factor auth page" do + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + app = insert(:oauth_app, scopes: ["read", "write", "follow"]) + + conn = + build_conn() + |> post("/oauth/authorize", %{ + "authorization" => %{ + "name" => user.nickname, + "password" => "test", + "client_id" => app.client_id, + "redirect_uri" => app.redirect_uris, + "scope" => "read write", + "state" => "statepassed" + } + }) + + result = html_response(conn, 200) + + mfa_token = Repo.get_by(MFA.Token, user_id: user.id) + assert result =~ app.redirect_uris + assert result =~ "statepassed" + assert result =~ mfa_token.token + assert result =~ "Two-factor authentication" + end + test "returns 401 for wrong credentials", %{conn: conn} do user = insert(:user) app = insert(:oauth_app) @@ -735,6 +772,46 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do assert token.scopes == app.scopes end + test "issues a mfa token for `password` grant_type, when MFA enabled" do + password = "testpassword" + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + password_hash: Comeonin.Pbkdf2.hashpwsalt(password), + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + app = insert(:oauth_app, scopes: ["read", "write"]) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "password", + "username" => user.nickname, + "password" => password, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(403) + + assert match?( + %{ + "supported_challenge_types" => "totp", + "mfa_token" => _, + "error" => "mfa_required" + }, + response + ) + + token = Repo.get_by(MFA.Token, token: response["mfa_token"]) + assert token.user_id == user.id + assert token.authorization_id + end + test "issues a token for request with HTTP basic auth client credentials" do user = insert(:user) app = insert(:oauth_app, scopes: ["scope1", "scope2", "scope3"]) diff --git a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs index 61a1689b9..43f1b154d 100644 --- a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs +++ b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs @@ -3,12 +3,14 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do + use Oban.Testing, repo: Pleroma.Repo use Pleroma.Web.ConnCase alias Pleroma.Conversation.Participation alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo + alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.Web.CommonAPI @@ -41,7 +43,9 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do other_user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"}) - {:ok, activity, _object} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + {:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + + ObanHelpers.perform_all() result = conn @@ -52,7 +56,9 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do assert %{"id" => id} = json_response(result, 200) assert to_string(activity.id) == id - object = Object.normalize(activity) + ObanHelpers.perform_all() + + object = Object.get_by_ap_id(activity.data["object"]) assert object.data["reaction_count"] == 0 end @@ -71,8 +77,8 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do assert result == [] - {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") - {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, doomed_user, "🎅") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, doomed_user, "🎅") User.perform(:delete, doomed_user) @@ -109,8 +115,8 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do assert result == [] - {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") - {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") result = conn diff --git a/test/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs b/test/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs new file mode 100644 index 000000000..d23d08a00 --- /dev/null +++ b/test/web/pleroma_api/controllers/two_factor_authentication_controller_test.exs @@ -0,0 +1,260 @@ +defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + alias Pleroma.MFA.Settings + alias Pleroma.MFA.TOTP + + describe "GET /api/pleroma/accounts/mfa/settings" do + test "returns user mfa settings for new user", %{conn: conn} do + token = insert(:oauth_token, scopes: ["read", "follow"]) + token2 = insert(:oauth_token, scopes: ["write"]) + + assert conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> get("/api/pleroma/accounts/mfa") + |> json_response(:ok) == %{ + "settings" => %{"enabled" => false, "totp" => false} + } + + assert conn + |> put_req_header("authorization", "Bearer #{token2.token}") + |> get("/api/pleroma/accounts/mfa") + |> json_response(403) == %{ + "error" => "Insufficient permissions: read:security." + } + end + + test "returns user mfa settings with enabled totp", %{conn: conn} do + user = + insert(:user, + multi_factor_authentication_settings: %Settings{ + enabled: true, + totp: %Settings.TOTP{secret: "XXX", delivery_type: "app", confirmed: true} + } + ) + + token = insert(:oauth_token, scopes: ["read", "follow"], user: user) + + assert conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> get("/api/pleroma/accounts/mfa") + |> json_response(:ok) == %{ + "settings" => %{"enabled" => true, "totp" => true} + } + end + end + + describe "GET /api/pleroma/accounts/mfa/backup_codes" do + test "returns backup codes", %{conn: conn} do + user = + insert(:user, + multi_factor_authentication_settings: %Settings{ + backup_codes: ["1", "2", "3"], + totp: %Settings.TOTP{secret: "secret"} + } + ) + + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + token2 = insert(:oauth_token, scopes: ["read"]) + + response = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> get("/api/pleroma/accounts/mfa/backup_codes") + |> json_response(:ok) + + assert [<<_::bytes-size(6)>>, <<_::bytes-size(6)>>] = response["codes"] + user = refresh_record(user) + mfa_settings = user.multi_factor_authentication_settings + assert mfa_settings.totp.secret == "secret" + refute mfa_settings.backup_codes == ["1", "2", "3"] + refute mfa_settings.backup_codes == [] + + assert conn + |> put_req_header("authorization", "Bearer #{token2.token}") + |> get("/api/pleroma/accounts/mfa/backup_codes") + |> json_response(403) == %{ + "error" => "Insufficient permissions: write:security." + } + end + end + + describe "GET /api/pleroma/accounts/mfa/setup/totp" do + test "return errors when method is invalid", %{conn: conn} do + user = insert(:user) + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + + response = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> get("/api/pleroma/accounts/mfa/setup/torf") + |> json_response(400) + + assert response == %{"error" => "undefined method"} + end + + test "returns key and provisioning_uri", %{conn: conn} do + user = + insert(:user, + multi_factor_authentication_settings: %Settings{backup_codes: ["1", "2", "3"]} + ) + + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + token2 = insert(:oauth_token, scopes: ["read"]) + + response = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> get("/api/pleroma/accounts/mfa/setup/totp") + |> json_response(:ok) + + user = refresh_record(user) + mfa_settings = user.multi_factor_authentication_settings + secret = mfa_settings.totp.secret + refute mfa_settings.enabled + assert mfa_settings.backup_codes == ["1", "2", "3"] + + assert response == %{ + "key" => secret, + "provisioning_uri" => TOTP.provisioning_uri(secret, "#{user.email}") + } + + assert conn + |> put_req_header("authorization", "Bearer #{token2.token}") + |> get("/api/pleroma/accounts/mfa/setup/totp") + |> json_response(403) == %{ + "error" => "Insufficient permissions: write:security." + } + end + end + + describe "GET /api/pleroma/accounts/mfa/confirm/totp" do + test "returns success result", %{conn: conn} do + secret = TOTP.generate_secret() + code = TOTP.generate_token(secret) + + user = + insert(:user, + multi_factor_authentication_settings: %Settings{ + backup_codes: ["1", "2", "3"], + totp: %Settings.TOTP{secret: secret} + } + ) + + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + token2 = insert(:oauth_token, scopes: ["read"]) + + assert conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: code}) + |> json_response(:ok) + + settings = refresh_record(user).multi_factor_authentication_settings + assert settings.enabled + assert settings.totp.secret == secret + assert settings.totp.confirmed + assert settings.backup_codes == ["1", "2", "3"] + + assert conn + |> put_req_header("authorization", "Bearer #{token2.token}") + |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: code}) + |> json_response(403) == %{ + "error" => "Insufficient permissions: write:security." + } + end + + test "returns error if password incorrect", %{conn: conn} do + secret = TOTP.generate_secret() + code = TOTP.generate_token(secret) + + user = + insert(:user, + multi_factor_authentication_settings: %Settings{ + backup_codes: ["1", "2", "3"], + totp: %Settings.TOTP{secret: secret} + } + ) + + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + + response = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "xxx", code: code}) + |> json_response(422) + + settings = refresh_record(user).multi_factor_authentication_settings + refute settings.enabled + refute settings.totp.confirmed + assert settings.backup_codes == ["1", "2", "3"] + assert response == %{"error" => "Invalid password."} + end + + test "returns error if code incorrect", %{conn: conn} do + secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %Settings{ + backup_codes: ["1", "2", "3"], + totp: %Settings.TOTP{secret: secret} + } + ) + + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + token2 = insert(:oauth_token, scopes: ["read"]) + + response = + conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: "code"}) + |> json_response(422) + + settings = refresh_record(user).multi_factor_authentication_settings + refute settings.enabled + refute settings.totp.confirmed + assert settings.backup_codes == ["1", "2", "3"] + assert response == %{"error" => "invalid_token"} + + assert conn + |> put_req_header("authorization", "Bearer #{token2.token}") + |> post("/api/pleroma/accounts/mfa/confirm/totp", %{password: "test", code: "code"}) + |> json_response(403) == %{ + "error" => "Insufficient permissions: write:security." + } + end + end + + describe "DELETE /api/pleroma/accounts/mfa/totp" do + test "returns success result", %{conn: conn} do + user = + insert(:user, + multi_factor_authentication_settings: %Settings{ + backup_codes: ["1", "2", "3"], + totp: %Settings.TOTP{secret: "secret"} + } + ) + + token = insert(:oauth_token, scopes: ["write", "follow"], user: user) + token2 = insert(:oauth_token, scopes: ["read"]) + + assert conn + |> put_req_header("authorization", "Bearer #{token.token}") + |> delete("/api/pleroma/accounts/mfa/totp", %{password: "test"}) + |> json_response(:ok) + + settings = refresh_record(user).multi_factor_authentication_settings + refute settings.enabled + assert settings.totp.secret == nil + refute settings.totp.confirmed + + assert conn + |> put_req_header("authorization", "Bearer #{token2.token}") + |> delete("/api/pleroma/accounts/mfa/totp", %{password: "test"}) + |> json_response(403) == %{ + "error" => "Insufficient permissions: write:security." + } + end + end +end diff --git a/test/web/plugs/plug_test.exs b/test/web/plugs/plug_test.exs new file mode 100644 index 000000000..943e484e7 --- /dev/null +++ b/test/web/plugs/plug_test.exs @@ -0,0 +1,91 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PlugTest do + @moduledoc "Tests for the functionality added via `use Pleroma.Web, :plug`" + + alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug + alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug + alias Pleroma.Plugs.PlugHelper + + import Mock + + use Pleroma.Web.ConnCase + + describe "when plug is skipped, " do + setup_with_mocks( + [ + {ExpectPublicOrAuthenticatedCheckPlug, [:passthrough], []} + ], + %{conn: conn} + ) do + conn = ExpectPublicOrAuthenticatedCheckPlug.skip_plug(conn) + %{conn: conn} + end + + test "it neither adds plug to called plugs list nor calls `perform/2`, " <> + "regardless of :if_func / :unless_func options", + %{conn: conn} do + for opts <- [%{}, %{if_func: fn _ -> true end}, %{unless_func: fn _ -> false end}] do + ret_conn = ExpectPublicOrAuthenticatedCheckPlug.call(conn, opts) + + refute called(ExpectPublicOrAuthenticatedCheckPlug.perform(:_, :_)) + refute PlugHelper.plug_called?(ret_conn, ExpectPublicOrAuthenticatedCheckPlug) + end + end + end + + describe "when plug is NOT skipped, " do + setup_with_mocks([{ExpectAuthenticatedCheckPlug, [:passthrough], []}]) do + :ok + end + + test "with no pre-run checks, adds plug to called plugs list and calls `perform/2`", %{ + conn: conn + } do + ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{}) + + assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_)) + assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) + end + + test "when :if_func option is given, calls the plug only if provided function evals tru-ish", + %{conn: conn} do + ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{if_func: fn _ -> false end}) + + refute called(ExpectAuthenticatedCheckPlug.perform(:_, :_)) + refute PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) + + ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{if_func: fn _ -> true end}) + + assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_)) + assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) + end + + test "if :unless_func option is given, calls the plug only if provided function evals falsy", + %{conn: conn} do + ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{unless_func: fn _ -> true end}) + + refute called(ExpectAuthenticatedCheckPlug.perform(:_, :_)) + refute PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) + + ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{unless_func: fn _ -> false end}) + + assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_)) + assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) + end + + test "allows a plug to be called multiple times (even if it's in called plugs list)", %{ + conn: conn + } do + conn = ExpectAuthenticatedCheckPlug.call(conn, %{an_option: :value1}) + assert called(ExpectAuthenticatedCheckPlug.perform(conn, %{an_option: :value1})) + + assert PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) + + conn = ExpectAuthenticatedCheckPlug.call(conn, %{an_option: :value2}) + assert called(ExpectAuthenticatedCheckPlug.perform(conn, %{an_option: :value2})) + end + end +end diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs index b2664bf28..b855d72ba 100644 --- a/test/web/push/impl_test.exs +++ b/test/web/push/impl_test.exs @@ -193,7 +193,7 @@ defmodule Pleroma.Web.Push.ImplTest do end describe "build_content/3" do - test "returns info content for direct message with enabled privacy option" do + test "hides details for notifications when privacy option enabled" do user = insert(:user, nickname: "Bob") user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: true}) @@ -209,12 +209,37 @@ defmodule Pleroma.Web.Push.ImplTest do object = Object.normalize(activity) assert Impl.build_content(notif, actor, object) == %{ - body: "@Bob", - title: "New Direct Message" + body: "New Direct Message" + } + + {:ok, activity} = + CommonAPI.post(user, %{ + "visibility" => "public", + "status" => "<Lorem ipsum dolor sit amet." + }) + + notif = insert(:notification, user: user2, activity: activity) + + actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) + object = Object.normalize(activity) + + assert Impl.build_content(notif, actor, object) == %{ + body: "New Mention" + } + + {:ok, activity} = CommonAPI.favorite(user, activity.id) + + notif = insert(:notification, user: user2, activity: activity) + + actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) + object = Object.normalize(activity) + + assert Impl.build_content(notif, actor, object) == %{ + body: "New Favorite" } end - test "returns regular content for direct message with disabled privacy option" do + test "returns regular content for notifications with privacy option disabled" do user = insert(:user, nickname: "Bob") user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: false}) @@ -235,6 +260,36 @@ defmodule Pleroma.Web.Push.ImplTest do "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini...", title: "New Direct Message" } + + {:ok, activity} = + CommonAPI.post(user, %{ + "visibility" => "public", + "status" => + "<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." + }) + + notif = insert(:notification, user: user2, activity: activity) + + actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) + object = Object.normalize(activity) + + assert Impl.build_content(notif, actor, object) == %{ + body: + "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini...", + title: "New Mention" + } + + {:ok, activity} = CommonAPI.favorite(user, activity.id) + + notif = insert(:notification, user: user2, activity: activity) + + actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) + object = Object.normalize(activity) + + assert Impl.build_content(notif, actor, object) == %{ + body: "@Bob has favorited your post", + title: "New Favorite" + } end end end diff --git a/test/web/streamer/ping_test.exs b/test/web/streamer/ping_test.exs deleted file mode 100644 index 5df6c1cc3..000000000 --- a/test/web/streamer/ping_test.exs +++ /dev/null @@ -1,36 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PingTest do - use Pleroma.DataCase - - import Pleroma.Factory - alias Pleroma.Web.Streamer - - setup do - start_supervised({Streamer.supervisor(), [ping_interval: 30]}) - - :ok - end - - describe "sockets" do - setup do - user = insert(:user) - {:ok, %{user: user}} - end - - test "it sends pings", %{user: user} do - task = - Task.async(fn -> - assert_receive {:text, received_event}, 40 - assert_receive {:text, received_event}, 40 - assert_receive {:text, received_event}, 40 - end) - - Streamer.add_socket("public", %{transport_pid: task.pid, assigns: %{user: user}}) - - Task.await(task) - end - end -end diff --git a/test/web/streamer/state_test.exs b/test/web/streamer/state_test.exs deleted file mode 100644 index a755e75c0..000000000 --- a/test/web/streamer/state_test.exs +++ /dev/null @@ -1,54 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.StateTest do - use Pleroma.DataCase - - import Pleroma.Factory - alias Pleroma.Web.Streamer - alias Pleroma.Web.Streamer.StreamerSocket - - @moduletag needs_streamer: true - - describe "sockets" do - setup do - user = insert(:user) - user2 = insert(:user) - {:ok, %{user: user, user2: user2}} - end - - test "it can add a socket", %{user: user} do - Streamer.add_socket("public", %{transport_pid: 1, assigns: %{user: user}}) - - assert(%{"public" => [%StreamerSocket{transport_pid: 1}]} = Streamer.get_sockets()) - end - - test "it can add multiple sockets per user", %{user: user} do - Streamer.add_socket("public", %{transport_pid: 1, assigns: %{user: user}}) - Streamer.add_socket("public", %{transport_pid: 2, assigns: %{user: user}}) - - assert( - %{ - "public" => [ - %StreamerSocket{transport_pid: 2}, - %StreamerSocket{transport_pid: 1} - ] - } = Streamer.get_sockets() - ) - end - - test "it will not add a duplicate socket", %{user: user} do - Streamer.add_socket("activity", %{transport_pid: 1, assigns: %{user: user}}) - Streamer.add_socket("activity", %{transport_pid: 1, assigns: %{user: user}}) - - assert( - %{ - "activity" => [ - %StreamerSocket{transport_pid: 1} - ] - } = Streamer.get_sockets() - ) - end - end -end diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs index 8b8d8af6c..ee530f4e9 100644 --- a/test/web/streamer/streamer_test.exs +++ b/test/web/streamer/streamer_test.exs @@ -12,13 +12,9 @@ defmodule Pleroma.Web.StreamerTest do alias Pleroma.User alias Pleroma.Web.CommonAPI alias Pleroma.Web.Streamer - alias Pleroma.Web.Streamer.StreamerSocket - alias Pleroma.Web.Streamer.Worker @moduletag needs_streamer: true, capture_log: true - @streamer_timeout 150 - @streamer_start_wait 10 setup do: clear_config([:instance, :skip_thread_containment]) describe "user streams" do @@ -29,69 +25,35 @@ defmodule Pleroma.Web.StreamerTest do end test "it streams the user's post in the 'user' stream", %{user: user} do - task = - Task.async(fn -> - assert_receive {:text, _}, @streamer_timeout - end) - - Streamer.add_socket( - "user", - %{transport_pid: task.pid, assigns: %{user: user}} - ) - + Streamer.add_socket("user", user) {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"}) - - Streamer.stream("user", activity) - Task.await(task) + assert_receive {:render_with_user, _, _, ^activity} + refute Streamer.filtered_by_user?(user, activity) end test "it streams boosts of the user in the 'user' stream", %{user: user} do - task = - Task.async(fn -> - assert_receive {:text, _}, @streamer_timeout - end) - - Streamer.add_socket( - "user", - %{transport_pid: task.pid, assigns: %{user: user}} - ) + Streamer.add_socket("user", user) other_user = insert(:user) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hey"}) {:ok, announce, _} = CommonAPI.repeat(activity.id, user) - Streamer.stream("user", announce) - Task.await(task) + assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce} + refute Streamer.filtered_by_user?(user, announce) end test "it sends notify to in the 'user' stream", %{user: user, notify: notify} do - task = - Task.async(fn -> - assert_receive {:text, _}, @streamer_timeout - end) - - Streamer.add_socket( - "user", - %{transport_pid: task.pid, assigns: %{user: user}} - ) - + Streamer.add_socket("user", user) Streamer.stream("user", notify) - Task.await(task) + assert_receive {:render_with_user, _, _, ^notify} + refute Streamer.filtered_by_user?(user, notify) end test "it sends notify to in the 'user:notification' stream", %{user: user, notify: notify} do - task = - Task.async(fn -> - assert_receive {:text, _}, @streamer_timeout - end) - - Streamer.add_socket( - "user:notification", - %{transport_pid: task.pid, assigns: %{user: user}} - ) - + Streamer.add_socket("user:notification", user) Streamer.stream("user:notification", notify) - Task.await(task) + assert_receive {:render_with_user, _, _, ^notify} + refute Streamer.filtered_by_user?(user, notify) end test "it doesn't send notify to the 'user:notification' stream when a user is blocked", %{ @@ -100,18 +62,12 @@ defmodule Pleroma.Web.StreamerTest do blocked = insert(:user) {:ok, _user_relationship} = User.block(user, blocked) - task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end) - - Streamer.add_socket( - "user:notification", - %{transport_pid: task.pid, assigns: %{user: user}} - ) + Streamer.add_socket("user:notification", user) {:ok, activity} = CommonAPI.post(user, %{"status" => ":("}) - {:ok, notif} = CommonAPI.favorite(blocked, activity.id) + {:ok, _} = CommonAPI.favorite(blocked, activity.id) - Streamer.stream("user:notification", notif) - Task.await(task) + refute_receive _ end test "it doesn't send notify to the 'user:notification' stream when a thread is muted", %{ @@ -119,45 +75,50 @@ defmodule Pleroma.Web.StreamerTest do } do user2 = insert(:user) - task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end) + {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"}) + {:ok, _} = CommonAPI.add_mute(user, activity) - Streamer.add_socket( - "user:notification", - %{transport_pid: task.pid, assigns: %{user: user}} - ) + Streamer.add_socket("user:notification", user) - {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"}) - {:ok, activity} = CommonAPI.add_mute(user, activity) - {:ok, notif} = CommonAPI.favorite(user2, activity.id) + {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id) - Streamer.stream("user:notification", notif) - Task.await(task) + refute_receive _ + assert Streamer.filtered_by_user?(user, favorite_activity) end - test "it doesn't send notify to the 'user:notification' stream' when a domain is blocked", %{ + test "it sends favorite to 'user:notification' stream'", %{ user: user } do user2 = insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/meanie"}) - task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end) + {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"}) + Streamer.add_socket("user:notification", user) + {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id) - Streamer.add_socket( - "user:notification", - %{transport_pid: task.pid, assigns: %{user: user}} - ) + assert_receive {:render_with_user, _, "notification.json", notif} + assert notif.activity.id == favorite_activity.id + refute Streamer.filtered_by_user?(user, notif) + end + + test "it doesn't send the 'user:notification' stream' when a domain is blocked", %{ + user: user + } do + user2 = insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/meanie"}) {:ok, user} = User.block_domain(user, "hecking-lewd-place.com") {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"}) - {:ok, notif} = CommonAPI.favorite(user2, activity.id) + Streamer.add_socket("user:notification", user) + {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id) - Streamer.stream("user:notification", notif) - Task.await(task) + refute_receive _ + assert Streamer.filtered_by_user?(user, favorite_activity) end test "it sends follow activities to the 'user:notification' stream", %{ user: user } do user_url = user.ap_id + user2 = insert(:user) body = File.read!("test/fixtures/users_mock/localhost.json") @@ -169,79 +130,57 @@ defmodule Pleroma.Web.StreamerTest do %Tesla.Env{status: 200, body: body} end) - user2 = insert(:user) - task = Task.async(fn -> assert_receive {:text, _}, @streamer_timeout end) - - Process.sleep(@streamer_start_wait) - - Streamer.add_socket( - "user:notification", - %{transport_pid: task.pid, assigns: %{user: user}} - ) - - {:ok, _follower, _followed, _activity} = CommonAPI.follow(user2, user) + Streamer.add_socket("user:notification", user) + {:ok, _follower, _followed, follow_activity} = CommonAPI.follow(user2, user) - # We don't directly pipe the notification to the streamer as it's already - # generated as a side effect of CommonAPI.follow(). - Task.await(task) + assert_receive {:render_with_user, _, "notification.json", notif} + assert notif.activity.id == follow_activity.id + refute Streamer.filtered_by_user?(user, notif) end end - test "it sends to public" do + test "it sends to public authenticated" do user = insert(:user) other_user = insert(:user) - task = - Task.async(fn -> - assert_receive {:text, _}, @streamer_timeout - end) + Streamer.add_socket("public", other_user) - fake_socket = %StreamerSocket{ - transport_pid: task.pid, - user: user - } + {:ok, activity} = CommonAPI.post(user, %{"status" => "Test"}) + assert_receive {:render_with_user, _, _, ^activity} + refute Streamer.filtered_by_user?(user, activity) + end + test "works for deletions" do + user = insert(:user) + other_user = insert(:user) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "Test"}) - topics = %{ - "public" => [fake_socket] - } - - Worker.push_to_socket(topics, "public", activity) - - Task.await(task) - - task = - Task.async(fn -> - expected_event = - %{ - "event" => "delete", - "payload" => activity.id - } - |> Jason.encode!() - - assert_receive {:text, received_event}, @streamer_timeout - assert received_event == expected_event - end) + Streamer.add_socket("public", user) - fake_socket = %StreamerSocket{ - transport_pid: task.pid, - user: user - } + {:ok, _} = CommonAPI.delete(activity.id, other_user) + activity_id = activity.id + assert_receive {:text, event} + assert %{"event" => "delete", "payload" => ^activity_id} = Jason.decode!(event) + end - {:ok, activity} = CommonAPI.delete(activity.id, other_user) + test "it sends to public unauthenticated" do + user = insert(:user) - topics = %{ - "public" => [fake_socket] - } + Streamer.add_socket("public", nil) - Worker.push_to_socket(topics, "public", activity) + {:ok, activity} = CommonAPI.post(user, %{"status" => "Test"}) + activity_id = activity.id + assert_receive {:text, event} + assert %{"event" => "update", "payload" => payload} = Jason.decode!(event) + assert %{"id" => ^activity_id} = Jason.decode!(payload) - Task.await(task) + {:ok, _} = CommonAPI.delete(activity.id, user) + assert_receive {:text, event} + assert %{"event" => "delete", "payload" => ^activity_id} = Jason.decode!(event) end describe "thread_containment" do - test "it doesn't send to user if recipients invalid and thread containment is enabled" do + test "it filters to user if recipients invalid and thread containment is enabled" do Pleroma.Config.put([:instance, :skip_thread_containment], false) author = insert(:user) user = insert(:user) @@ -256,12 +195,10 @@ defmodule Pleroma.Web.StreamerTest do ) ) - task = Task.async(fn -> refute_receive {:text, _}, 1_000 end) - fake_socket = %StreamerSocket{transport_pid: task.pid, user: user} - topics = %{"public" => [fake_socket]} - Worker.push_to_socket(topics, "public", activity) - - Task.await(task) + Streamer.add_socket("public", user) + Streamer.stream("public", activity) + assert_receive {:render_with_user, _, _, ^activity} + assert Streamer.filtered_by_user?(user, activity) end test "it sends message if recipients invalid and thread containment is disabled" do @@ -279,12 +216,11 @@ defmodule Pleroma.Web.StreamerTest do ) ) - task = Task.async(fn -> assert_receive {:text, _}, 1_000 end) - fake_socket = %StreamerSocket{transport_pid: task.pid, user: user} - topics = %{"public" => [fake_socket]} - Worker.push_to_socket(topics, "public", activity) + Streamer.add_socket("public", user) + Streamer.stream("public", activity) - Task.await(task) + assert_receive {:render_with_user, _, _, ^activity} + refute Streamer.filtered_by_user?(user, activity) end test "it sends message if recipients invalid and thread containment is enabled but user's thread containment is disabled" do @@ -302,255 +238,168 @@ defmodule Pleroma.Web.StreamerTest do ) ) - task = Task.async(fn -> assert_receive {:text, _}, 1_000 end) - fake_socket = %StreamerSocket{transport_pid: task.pid, user: user} - topics = %{"public" => [fake_socket]} - Worker.push_to_socket(topics, "public", activity) + Streamer.add_socket("public", user) + Streamer.stream("public", activity) - Task.await(task) + assert_receive {:render_with_user, _, _, ^activity} + refute Streamer.filtered_by_user?(user, activity) end end describe "blocks" do - test "it doesn't send messages involving blocked users" do + test "it filters messages involving blocked users" do user = insert(:user) blocked_user = insert(:user) {:ok, _user_relationship} = User.block(user, blocked_user) + Streamer.add_socket("public", user) {:ok, activity} = CommonAPI.post(blocked_user, %{"status" => "Test"}) - - task = - Task.async(fn -> - refute_receive {:text, _}, 1_000 - end) - - fake_socket = %StreamerSocket{ - transport_pid: task.pid, - user: user - } - - topics = %{ - "public" => [fake_socket] - } - - Worker.push_to_socket(topics, "public", activity) - - Task.await(task) + assert_receive {:render_with_user, _, _, ^activity} + assert Streamer.filtered_by_user?(user, activity) end - test "it doesn't send messages transitively involving blocked users" do + test "it filters messages transitively involving blocked users" do blocker = insert(:user) blockee = insert(:user) friend = insert(:user) - task = - Task.async(fn -> - refute_receive {:text, _}, 1_000 - end) - - fake_socket = %StreamerSocket{ - transport_pid: task.pid, - user: blocker - } - - topics = %{ - "public" => [fake_socket] - } + Streamer.add_socket("public", blocker) {:ok, _user_relationship} = User.block(blocker, blockee) {:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey! @#{blockee.nickname}"}) - Worker.push_to_socket(topics, "public", activity_one) + assert_receive {:render_with_user, _, _, ^activity_one} + assert Streamer.filtered_by_user?(blocker, activity_one) {:ok, activity_two} = CommonAPI.post(blockee, %{"status" => "hey! @#{friend.nickname}"}) - Worker.push_to_socket(topics, "public", activity_two) + assert_receive {:render_with_user, _, _, ^activity_two} + assert Streamer.filtered_by_user?(blocker, activity_two) {:ok, activity_three} = CommonAPI.post(blockee, %{"status" => "hey! @#{blocker.nickname}"}) - Worker.push_to_socket(topics, "public", activity_three) - - Task.await(task) + assert_receive {:render_with_user, _, _, ^activity_three} + assert Streamer.filtered_by_user?(blocker, activity_three) end end - test "it doesn't send unwanted DMs to list" do - user_a = insert(:user) - user_b = insert(:user) - user_c = insert(:user) - - {:ok, user_a} = User.follow(user_a, user_b) - - {:ok, list} = List.create("Test", user_a) - {:ok, list} = List.follow(list, user_b) - - {:ok, activity} = - CommonAPI.post(user_b, %{ - "status" => "@#{user_c.nickname} Test", - "visibility" => "direct" - }) - - task = - Task.async(fn -> - refute_receive {:text, _}, 1_000 - end) - - fake_socket = %StreamerSocket{ - transport_pid: task.pid, - user: user_a - } - - topics = %{ - "list:#{list.id}" => [fake_socket] - } - - Worker.handle_call({:stream, "list", activity}, self(), topics) - - Task.await(task) - end - - test "it doesn't send unwanted private posts to list" do - user_a = insert(:user) - user_b = insert(:user) + describe "lists" do + test "it doesn't send unwanted DMs to list" do + user_a = insert(:user) + user_b = insert(:user) + user_c = insert(:user) - {:ok, list} = List.create("Test", user_a) - {:ok, list} = List.follow(list, user_b) + {:ok, user_a} = User.follow(user_a, user_b) - {:ok, activity} = - CommonAPI.post(user_b, %{ - "status" => "Test", - "visibility" => "private" - }) + {:ok, list} = List.create("Test", user_a) + {:ok, list} = List.follow(list, user_b) - task = - Task.async(fn -> - refute_receive {:text, _}, 1_000 - end) + Streamer.add_socket("list:#{list.id}", user_a) - fake_socket = %StreamerSocket{ - transport_pid: task.pid, - user: user_a - } + {:ok, _activity} = + CommonAPI.post(user_b, %{ + "status" => "@#{user_c.nickname} Test", + "visibility" => "direct" + }) - topics = %{ - "list:#{list.id}" => [fake_socket] - } + refute_receive _ + end - Worker.handle_call({:stream, "list", activity}, self(), topics) + test "it doesn't send unwanted private posts to list" do + user_a = insert(:user) + user_b = insert(:user) - Task.await(task) - end + {:ok, list} = List.create("Test", user_a) + {:ok, list} = List.follow(list, user_b) - test "it sends wanted private posts to list" do - user_a = insert(:user) - user_b = insert(:user) + Streamer.add_socket("list:#{list.id}", user_a) - {:ok, user_a} = User.follow(user_a, user_b) + {:ok, _activity} = + CommonAPI.post(user_b, %{ + "status" => "Test", + "visibility" => "private" + }) - {:ok, list} = List.create("Test", user_a) - {:ok, list} = List.follow(list, user_b) + refute_receive _ + end - {:ok, activity} = - CommonAPI.post(user_b, %{ - "status" => "Test", - "visibility" => "private" - }) + test "it sends wanted private posts to list" do + user_a = insert(:user) + user_b = insert(:user) - task = - Task.async(fn -> - assert_receive {:text, _}, 1_000 - end) + {:ok, user_a} = User.follow(user_a, user_b) - fake_socket = %StreamerSocket{ - transport_pid: task.pid, - user: user_a - } + {:ok, list} = List.create("Test", user_a) + {:ok, list} = List.follow(list, user_b) - Streamer.add_socket( - "list:#{list.id}", - fake_socket - ) + Streamer.add_socket("list:#{list.id}", user_a) - Worker.handle_call({:stream, "list", activity}, self(), %{}) + {:ok, activity} = + CommonAPI.post(user_b, %{ + "status" => "Test", + "visibility" => "private" + }) - Task.await(task) + assert_receive {:render_with_user, _, _, ^activity} + refute Streamer.filtered_by_user?(user_a, activity) + end end - test "it doesn't send muted reblogs" do - user1 = insert(:user) - user2 = insert(:user) - user3 = insert(:user) - CommonAPI.hide_reblogs(user1, user2) - - {:ok, create_activity} = CommonAPI.post(user3, %{"status" => "I'm kawen"}) - {:ok, announce_activity, _} = CommonAPI.repeat(create_activity.id, user2) - - task = - Task.async(fn -> - refute_receive {:text, _}, 1_000 - end) - - fake_socket = %StreamerSocket{ - transport_pid: task.pid, - user: user1 - } - - topics = %{ - "public" => [fake_socket] - } - - Worker.push_to_socket(topics, "public", announce_activity) + describe "muted reblogs" do + test "it filters muted reblogs" do + user1 = insert(:user) + user2 = insert(:user) + user3 = insert(:user) + CommonAPI.follow(user1, user2) + CommonAPI.hide_reblogs(user1, user2) - Task.await(task) - end + {:ok, create_activity} = CommonAPI.post(user3, %{"status" => "I'm kawen"}) - test "it does send non-reblog notification for reblog-muted actors" do - user1 = insert(:user) - user2 = insert(:user) - user3 = insert(:user) - CommonAPI.hide_reblogs(user1, user2) + Streamer.add_socket("user", user1) + {:ok, announce_activity, _} = CommonAPI.repeat(create_activity.id, user2) + assert_receive {:render_with_user, _, _, ^announce_activity} + assert Streamer.filtered_by_user?(user1, announce_activity) + end - {:ok, create_activity} = CommonAPI.post(user3, %{"status" => "I'm kawen"}) - {:ok, favorite_activity} = CommonAPI.favorite(user2, create_activity.id) + test "it filters reblog notification for reblog-muted actors" do + user1 = insert(:user) + user2 = insert(:user) + CommonAPI.follow(user1, user2) + CommonAPI.hide_reblogs(user1, user2) - task = - Task.async(fn -> - assert_receive {:text, _}, 1_000 - end) + {:ok, create_activity} = CommonAPI.post(user1, %{"status" => "I'm kawen"}) + Streamer.add_socket("user", user1) + {:ok, _favorite_activity, _} = CommonAPI.repeat(create_activity.id, user2) - fake_socket = %StreamerSocket{ - transport_pid: task.pid, - user: user1 - } + assert_receive {:render_with_user, _, "notification.json", notif} + assert Streamer.filtered_by_user?(user1, notif) + end - topics = %{ - "public" => [fake_socket] - } + test "it send non-reblog notification for reblog-muted actors" do + user1 = insert(:user) + user2 = insert(:user) + CommonAPI.follow(user1, user2) + CommonAPI.hide_reblogs(user1, user2) - Worker.push_to_socket(topics, "public", favorite_activity) + {:ok, create_activity} = CommonAPI.post(user1, %{"status" => "I'm kawen"}) + Streamer.add_socket("user", user1) + {:ok, _favorite_activity} = CommonAPI.favorite(user2, create_activity.id) - Task.await(task) + assert_receive {:render_with_user, _, "notification.json", notif} + refute Streamer.filtered_by_user?(user1, notif) + end end - test "it doesn't send posts from muted threads" do + test "it filters posts from muted threads" do user = insert(:user) user2 = insert(:user) + Streamer.add_socket("user", user2) {:ok, user2, user, _activity} = CommonAPI.follow(user2, user) - {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"}) - - {:ok, activity} = CommonAPI.add_mute(user2, activity) - - task = Task.async(fn -> refute_receive {:text, _}, @streamer_timeout end) - - Streamer.add_socket( - "user", - %{transport_pid: task.pid, assigns: %{user: user2}} - ) - - Streamer.stream("user", activity) - Task.await(task) + {:ok, _} = CommonAPI.add_mute(user2, activity) + assert_receive {:render_with_user, _, _, ^activity} + assert Streamer.filtered_by_user?(user2, activity) end describe "direct streams" do @@ -562,22 +411,7 @@ defmodule Pleroma.Web.StreamerTest do user = insert(:user) another_user = insert(:user) - task = - Task.async(fn -> - assert_receive {:text, received_event}, @streamer_timeout - - assert %{"event" => "conversation", "payload" => received_payload} = - Jason.decode!(received_event) - - assert %{"last_status" => last_status} = Jason.decode!(received_payload) - [participation] = Participation.for_user(user) - assert last_status["pleroma"]["direct_conversation_id"] == participation.id - end) - - Streamer.add_socket( - "direct", - %{transport_pid: task.pid, assigns: %{user: user}} - ) + Streamer.add_socket("direct", user) {:ok, _create_activity} = CommonAPI.post(another_user, %{ @@ -585,42 +419,47 @@ defmodule Pleroma.Web.StreamerTest do "visibility" => "direct" }) - Task.await(task) + assert_receive {:text, received_event} + + assert %{"event" => "conversation", "payload" => received_payload} = + Jason.decode!(received_event) + + assert %{"last_status" => last_status} = Jason.decode!(received_payload) + [participation] = Participation.for_user(user) + assert last_status["pleroma"]["direct_conversation_id"] == participation.id end test "it doesn't send conversation update to the 'direct' stream when the last message in the conversation is deleted" do user = insert(:user) another_user = insert(:user) + Streamer.add_socket("direct", user) + {:ok, create_activity} = CommonAPI.post(another_user, %{ "status" => "hi @#{user.nickname}", "visibility" => "direct" }) - task = - Task.async(fn -> - assert_receive {:text, received_event}, @streamer_timeout - assert %{"event" => "delete", "payload" => _} = Jason.decode!(received_event) + create_activity_id = create_activity.id + assert_receive {:render_with_user, _, _, ^create_activity} + assert_receive {:text, received_conversation1} + assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1) - refute_receive {:text, _}, @streamer_timeout - end) + {:ok, _} = CommonAPI.delete(create_activity_id, another_user) - Process.sleep(@streamer_start_wait) + assert_receive {:text, received_event} - Streamer.add_socket( - "direct", - %{transport_pid: task.pid, assigns: %{user: user}} - ) + assert %{"event" => "delete", "payload" => ^create_activity_id} = + Jason.decode!(received_event) - {:ok, _} = CommonAPI.delete(create_activity.id, another_user) - - Task.await(task) + refute_receive _ end test "it sends conversation update to the 'direct' stream when a message is deleted" do user = insert(:user) another_user = insert(:user) + Streamer.add_socket("direct", user) {:ok, create_activity} = CommonAPI.post(another_user, %{ @@ -630,35 +469,30 @@ defmodule Pleroma.Web.StreamerTest do {:ok, create_activity2} = CommonAPI.post(another_user, %{ - "status" => "hi @#{user.nickname}", + "status" => "hi @#{user.nickname} 2", "in_reply_to_status_id" => create_activity.id, "visibility" => "direct" }) - task = - Task.async(fn -> - assert_receive {:text, received_event}, @streamer_timeout - assert %{"event" => "delete", "payload" => _} = Jason.decode!(received_event) - - assert_receive {:text, received_event}, @streamer_timeout + assert_receive {:render_with_user, _, _, ^create_activity} + assert_receive {:render_with_user, _, _, ^create_activity2} + assert_receive {:text, received_conversation1} + assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1) + assert_receive {:text, received_conversation1} + assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1) - assert %{"event" => "conversation", "payload" => received_payload} = - Jason.decode!(received_event) - - assert %{"last_status" => last_status} = Jason.decode!(received_payload) - assert last_status["id"] == to_string(create_activity.id) - end) + {:ok, _} = CommonAPI.delete(create_activity2.id, another_user) - Process.sleep(@streamer_start_wait) + assert_receive {:text, received_event} + assert %{"event" => "delete", "payload" => _} = Jason.decode!(received_event) - Streamer.add_socket( - "direct", - %{transport_pid: task.pid, assigns: %{user: user}} - ) + assert_receive {:text, received_event} - {:ok, _} = CommonAPI.delete(create_activity2.id, another_user) + assert %{"event" => "conversation", "payload" => received_payload} = + Jason.decode!(received_event) - Task.await(task) + assert %{"last_status" => last_status} = Jason.decode!(received_payload) + assert last_status["id"] == to_string(create_activity.id) end end end diff --git a/test/web/twitter_api/remote_follow_controller_test.exs b/test/web/twitter_api/remote_follow_controller_test.exs index 5ff8694a8..f7e54c26a 100644 --- a/test/web/twitter_api/remote_follow_controller_test.exs +++ b/test/web/twitter_api/remote_follow_controller_test.exs @@ -6,11 +6,14 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do use Pleroma.Web.ConnCase alias Pleroma.Config + alias Pleroma.MFA + alias Pleroma.MFA.TOTP alias Pleroma.User alias Pleroma.Web.CommonAPI import ExUnit.CaptureLog import Pleroma.Factory + import Ecto.Query setup do Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) @@ -160,6 +163,119 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do end end + describe "POST /ostatus_subscribe - follow/2 with enabled Two-Factor Auth " do + test "render the MFA login form", %{conn: conn} do + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + user2 = insert(:user) + + response = + conn + |> post(remote_follow_path(conn, :do_follow), %{ + "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} + }) + |> response(200) + + mfa_token = Pleroma.Repo.one(from(q in Pleroma.MFA.Token, where: q.user_id == ^user.id)) + + assert response =~ "Two-factor authentication" + assert response =~ "Authentication code" + assert response =~ mfa_token.token + refute user2.follower_address in User.following(user) + end + + test "returns error when password is incorrect", %{conn: conn} do + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + user2 = insert(:user) + + response = + conn + |> post(remote_follow_path(conn, :do_follow), %{ + "authorization" => %{"name" => user.nickname, "password" => "test1", "id" => user2.id} + }) + |> response(200) + + assert response =~ "Wrong username or password" + refute user2.follower_address in User.following(user) + end + + test "follows", %{conn: conn} do + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + {:ok, %{token: token}} = MFA.Token.create_token(user) + + user2 = insert(:user) + otp_token = TOTP.generate_token(otp_secret) + + conn = + conn + |> post( + remote_follow_path(conn, :do_follow), + %{ + "mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id} + } + ) + + assert redirected_to(conn) == "/users/#{user2.id}" + assert user2.follower_address in User.following(user) + end + + test "returns error when auth code is incorrect", %{conn: conn} do + otp_secret = TOTP.generate_secret() + + user = + insert(:user, + multi_factor_authentication_settings: %MFA.Settings{ + enabled: true, + totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true} + } + ) + + {:ok, %{token: token}} = MFA.Token.create_token(user) + + user2 = insert(:user) + otp_token = TOTP.generate_token(TOTP.generate_secret()) + + response = + conn + |> post( + remote_follow_path(conn, :do_follow), + %{ + "mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id} + } + ) + |> response(200) + + assert response =~ "Wrong authentication code" + refute user2.follower_address in User.following(user) + end + end + describe "POST /ostatus_subscribe - follow/2 without assigned user " do test "follows", %{conn: conn} do user = insert(:user) diff --git a/test/web/web_finger/web_finger_test.exs b/test/web/web_finger/web_finger_test.exs index 4b4282727..f4884e0a2 100644 --- a/test/web/web_finger/web_finger_test.exs +++ b/test/web/web_finger/web_finger_test.exs @@ -67,7 +67,7 @@ defmodule Pleroma.Web.WebFingerTest do assert data["magic_key"] == nil assert data["salmon"] == nil - assert data["topic"] == "https://mstdn.jp/users/kPherox.atom" + assert data["topic"] == nil assert data["subject"] == "acct:kPherox@mstdn.jp" assert data["ap_id"] == "https://mstdn.jp/users/kPherox" assert data["subscribe_address"] == "https://mstdn.jp/authorize_interaction?acct={uri}" |