# Pleroma: A lightweight social networking server # Copyright © 2017-2021 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do use Pleroma.Web.ConnCase, async: false use Oban.Testing, repo: Pleroma.Repo alias Pleroma.Activity alias Pleroma.Delivery alias Pleroma.Instances alias Pleroma.Object alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ObjectView alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.CommonAPI alias Pleroma.Web.Endpoint alias Pleroma.Workers.ReceiverWorker import Pleroma.Factory require Pleroma.Constants setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) :ok end setup do: clear_config([:instance, :federating], true) setup do: clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) describe "/relay" do setup do: clear_config([:instance, :allow_relay], true) test "with the relay active, it returns the relay user", %{conn: conn} do res = conn |> get(~p"/relay") |> json_response(200) assert res["id"] =~ "/relay" end test "with the relay disabled, it returns 404", %{conn: conn} do clear_config([:instance, :allow_relay], false) conn |> get(~p"/relay") |> json_response(404) end test "on non-federating instance, it returns 404", %{conn: conn} do clear_config([:instance, :federating], false) user = insert(:user) conn |> assign(:user, user) |> get(~p"/relay") |> json_response(404) end end describe "/internal/fetch" do test "it returns the internal fetch user", %{conn: conn} do res = conn |> get(~p"/internal/fetch") |> json_response(200) assert res["id"] =~ "/fetch" end test "on non-federating instance, it returns 404", %{conn: conn} do clear_config([:instance, :federating], false) user = insert(:user) conn |> assign(:user, user) |> get(~p"/internal/fetch") |> json_response(404) end end describe "/users/:nickname" do test "it returns a json representation of the user with accept application/json", %{ conn: conn } do user = insert(:user) conn = conn |> put_req_header("accept", "application/json") |> get("/users/#{user.nickname}") user = User.get_cached_by_id(user.id) assert json_response(conn, 200) == UserView.render("user.json", %{user: user}) end test "it returns a json representation of the user with accept application/activity+json", %{ conn: conn } do user = insert(:user) conn = conn |> put_req_header("accept", "application/activity+json") |> get("/users/#{user.nickname}") user = User.get_cached_by_id(user.id) assert json_response(conn, 200) == UserView.render("user.json", %{user: user}) end test "it returns a json representation of the user with accept application/ld+json", %{ conn: conn } do user = insert(:user) conn = conn |> put_req_header( "accept", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" ) |> get("/users/#{user.nickname}") user = User.get_cached_by_id(user.id) assert json_response(conn, 200) == UserView.render("user.json", %{user: user}) end test "it returns 404 for remote users", %{ conn: conn } do user = insert(:user, local: false, nickname: "remoteuser@example.com") conn = conn |> put_req_header("accept", "application/json") |> get("/users/#{user.nickname}.json") assert json_response(conn, 404) end test "it returns error when user is not found", %{conn: conn} do response = conn |> put_req_header("accept", "application/json") |> get("/users/jimm") |> json_response(404) assert response == "Not found" end end describe "mastodon compatibility routes" do test "it returns a json representation of the object with accept application/json", %{ conn: conn } do {:ok, object} = %{ "type" => "Note", "content" => "hey", "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999", "actor" => Endpoint.url() <> "/users/raymoo", "to" => [Pleroma.Constants.as_public()] } |> Object.create() conn = conn |> put_req_header("accept", "application/json") |> get("/users/raymoo/statuses/999999999") assert json_response(conn, 200) == ObjectView.render("object.json", %{object: object}) end test "it returns a json representation of the activity with accept application/json", %{ conn: conn } do {:ok, object} = %{ "type" => "Note", "content" => "hey", "id" => Endpoint.url() <> "/users/raymoo/statuses/999999999", "actor" => Endpoint.url() <> "/users/raymoo", "to" => [Pleroma.Constants.as_public()] } |> Object.create() {:ok, activity, _} = %{ "id" => object.data["id"] <> "/activity", "type" => "Create", "object" => object.data["id"], "actor" => object.data["actor"], "to" => object.data["to"] } |> ActivityPub.persist(local: true) conn = conn |> put_req_header("accept", "application/json") |> get("/users/raymoo/statuses/999999999/activity") assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity}) end end describe "/objects/:uuid" do test "it doesn't return a local-only object", %{conn: conn} do user = insert(:user) {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"}) assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post) object = Object.normalize(post, fetch: false) uuid = String.split(object.data["id"], "/") |> List.last() conn = conn |> put_req_header("accept", "application/json") |> get("/objects/#{uuid}") assert json_response(conn, 404) end test "returns local-only objects when authenticated", %{conn: conn} do user = insert(:user) {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"}) assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post) object = Object.normalize(post, fetch: false) uuid = String.split(object.data["id"], "/") |> List.last() assert response = conn |> assign(:user, user) |> put_req_header("accept", "application/activity+json") |> get("/objects/#{uuid}") assert json_response(response, 200) == ObjectView.render("object.json", %{object: object}) end test "does not return local-only objects for remote users", %{conn: conn} do user = insert(:user) reader = insert(:user, local: false) {:ok, post} = CommonAPI.post(user, %{status: "test @#{reader.nickname}", visibility: "local"}) assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post) object = Object.normalize(post, fetch: false) uuid = String.split(object.data["id"], "/") |> List.last() assert response = conn |> assign(:user, reader) |> put_req_header("accept", "application/activity+json") |> get("/objects/#{uuid}") json_response(response, 404) end test "it returns a json representation of the object with accept application/json", %{ conn: conn } do note = insert(:note) uuid = String.split(note.data["id"], "/") |> List.last() conn = conn |> put_req_header("accept", "application/json") |> get("/objects/#{uuid}") assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note}) end test "it returns a json representation of the object with accept application/activity+json", %{conn: conn} do note = insert(:note) uuid = String.split(note.data["id"], "/") |> List.last() conn = conn |> put_req_header("accept", "application/activity+json") |> get("/objects/#{uuid}") assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note}) end test "it returns a json representation of the object with accept application/ld+json", %{ conn: conn } do note = insert(:note) uuid = String.split(note.data["id"], "/") |> List.last() conn = conn |> put_req_header( "accept", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" ) |> get("/objects/#{uuid}") assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note}) end test "does not cache authenticated response", %{conn: conn} do user = insert(:user) reader = insert(:user) {:ok, post} = CommonAPI.post(user, %{status: "test @#{reader.nickname}", visibility: "local"}) object = Object.normalize(post, fetch: false) uuid = String.split(object.data["id"], "/") |> List.last() assert response = conn |> assign(:user, reader) |> put_req_header("accept", "application/activity+json") |> get("/objects/#{uuid}") json_response(response, 200) conn |> put_req_header("accept", "application/activity+json") |> get("/objects/#{uuid}") |> json_response(404) end test "it returns 404 for non-public messages", %{conn: conn} do note = insert(:direct_note) uuid = String.split(note.data["id"], "/") |> List.last() conn = conn |> put_req_header("accept", "application/activity+json") |> get("/objects/#{uuid}") assert json_response(conn, 404) end test "returns visible non-public messages when authenticated", %{conn: conn} do note = insert(:direct_note) uuid = String.split(note.data["id"], "/") |> List.last() user = User.get_by_ap_id(note.data["actor"]) marisa = insert(:user) assert conn |> assign(:user, marisa) |> put_req_header("accept", "application/activity+json") |> get("/objects/#{uuid}") |> json_response(404) assert response = conn |> assign(:user, user) |> put_req_header("accept", "application/activity+json") |> get("/objects/#{uuid}") |> json_response(200) assert response == ObjectView.render("object.json", %{object: note}) end test "it returns 404 for tombstone objects", %{conn: conn} do tombstone = insert(:tombstone) uuid = String.split(tombstone.data["id"], "/") |> List.last() conn = conn |> put_req_header("accept", "application/activity+json") |> get("/objects/#{uuid}") assert json_response(conn, 404) end test "it caches a response", %{conn: conn} do note = insert(:note) uuid = String.split(note.data["id"], "/") |> List.last() conn1 = conn |> put_req_header("accept", "application/activity+json") |> get("/objects/#{uuid}") assert json_response(conn1, :ok) assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) conn2 = conn |> put_req_header("accept", "application/activity+json") |> get("/objects/#{uuid}") assert json_response(conn1, :ok) == json_response(conn2, :ok) assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"})) end test "cached purged after object deletion", %{conn: conn} do note = insert(:note) uuid = String.split(note.data["id"], "/") |> List.last() conn1 = conn |> put_req_header("accept", "application/activity+json") |> get("/objects/#{uuid}") assert json_response(conn1, :ok) assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) Object.delete(note) conn2 = conn |> put_req_header("accept", "application/activity+json") |> get("/objects/#{uuid}") assert "Not found" == json_response(conn2, :not_found) end end describe "/activities/:uuid" do test "it doesn't return a local-only activity", %{conn: conn} do user = insert(:user) {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"}) assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post) uuid = String.split(post.data["id"], "/") |> List.last() conn = conn |> put_req_header("accept", "application/json") |> get("/activities/#{uuid}") assert json_response(conn, 404) end test "returns local-only activities when authenticated", %{conn: conn} do user = insert(:user) {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"}) assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post) uuid = String.split(post.data["id"], "/") |> List.last() assert response = conn |> assign(:user, user) |> put_req_header("accept", "application/activity+json") |> get("/activities/#{uuid}") assert json_response(response, 200) == ObjectView.render("object.json", %{object: post}) end test "it returns a json representation of the activity", %{conn: conn} do activity = insert(:note_activity) uuid = String.split(activity.data["id"], "/") |> List.last() conn = conn |> put_req_header("accept", "application/activity+json") |> get("/activities/#{uuid}") assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity}) end test "it returns 404 for non-public activities", %{conn: conn} do activity = insert(:direct_note_activity) uuid = String.split(activity.data["id"], "/") |> List.last() conn = conn |> put_req_header("accept", "application/activity+json") |> get("/activities/#{uuid}") assert json_response(conn, 404) end test "returns visible non-public messages when authenticated", %{conn: conn} do note = insert(:direct_note_activity) uuid = String.split(note.data["id"], "/") |> List.last() user = User.get_by_ap_id(note.data["actor"]) marisa = insert(:user) assert conn |> assign(:user, marisa) |> put_req_header("accept", "application/activity+json") |> get("/activities/#{uuid}") |> json_response(404) assert response = conn |> assign(:user, user) |> put_req_header("accept", "application/activity+json") |> get("/activities/#{uuid}") |> json_response(200) assert response == ObjectView.render("object.json", %{object: note}) end test "it caches a response", %{conn: conn} do activity = insert(:note_activity) uuid = String.split(activity.data["id"], "/") |> List.last() conn1 = conn |> put_req_header("accept", "application/activity+json") |> get("/activities/#{uuid}") assert json_response(conn1, :ok) assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) conn2 = conn |> put_req_header("accept", "application/activity+json") |> get("/activities/#{uuid}") assert json_response(conn1, :ok) == json_response(conn2, :ok) assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"})) end test "cached purged after activity deletion", %{conn: conn} do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "cofe"}) uuid = String.split(activity.data["id"], "/") |> List.last() conn1 = conn |> put_req_header("accept", "application/activity+json") |> get("/activities/#{uuid}") assert json_response(conn1, :ok) assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"})) Activity.delete_all_by_object_ap_id(activity.object.data["id"]) conn2 = conn |> put_req_header("accept", "application/activity+json") |> get("/activities/#{uuid}") assert "Not found" == json_response(conn2, :not_found) end end describe "/inbox" do test "it inserts an incoming activity into the database", %{conn: conn} do data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!() conn = conn |> assign(:valid_signature, true) |> put_req_header( "signature", "keyId=\"http://mastodon.example.org/users/admin/main-key\"" ) |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) assert "ok" == json_response(conn, 200) ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) assert Activity.get_by_ap_id(data["id"]) end @tag capture_log: true test "it inserts an incoming activity into the database" <> "even if we can't fetch the user but have it in our db", %{conn: conn} do user = insert(:user, ap_id: "https://mastodon.example.org/users/raymoo", ap_enabled: true, local: false, last_refreshed_at: nil ) |> with_signing_key() data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!() |> Map.put("actor", user.ap_id) |> put_in(["object", "attributedTo"], user.ap_id) conn = conn |> assign(:valid_signature, true) |> put_req_header("signature", "keyId=\"#{user.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) assert "ok" == json_response(conn, 200) ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) assert Activity.get_by_ap_id(data["id"]) end test "it clears `unreachable` federation status of the sender", %{conn: conn} do data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!() sender_url = data["actor"] sender = insert(:user, ap_id: data["actor"]) |> with_signing_key() Instances.set_consistently_unreachable(sender_url) refute Instances.reachable?(sender_url) conn = conn |> assign(:valid_signature, true) |> put_req_header("signature", "keyId=\"#{sender.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) assert "ok" == json_response(conn, 200) assert Instances.reachable?(sender_url) end test "accept follow activity", %{conn: conn} do clear_config([:instance, :federating], true) relay = Relay.get_actor() assert {:ok, %Activity{} = activity} = Relay.follow("https://relay.mastodon.host/actor") followed_relay = Pleroma.User.get_by_ap_id("https://relay.mastodon.host/actor") relay = refresh_record(relay) accept = File.read!("test/fixtures/relay/accept-follow.json") |> String.replace("{{ap_id}}", relay.ap_id) |> String.replace("{{activity_id}}", activity.data["id"]) assert "ok" == conn |> assign(:valid_signature, true) |> put_req_header("signature", "keyId=\"#{followed_relay.ap_id}/main-key\"") |> put_req_header("content-type", "application/activity+json") |> post("/inbox", accept) |> json_response(200) ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) assert Pleroma.FollowingRelationship.following?( relay, followed_relay ) Mix.shell(Mix.Shell.Process) on_exit(fn -> Mix.shell(Mix.Shell.IO) end) :ok = Mix.Tasks.Pleroma.Relay.run(["list"]) assert_receive {:mix_shell, :info, ["https://relay.mastodon.host/actor"]} end test "accepts Add/Remove activities", %{conn: conn} do object_id = "c61d6733-e256-4fe1-ab13-1e369789423f" status = File.read!("test/fixtures/statuses/note.json") |> String.replace("{{nickname}}", "lain") |> String.replace("{{object_id}}", object_id) object_url = "https://example.com/objects/#{object_id}" user = File.read!("test/fixtures/users_mock/user.json") |> String.replace("{{nickname}}", "lain") actor = "https://example.com/users/lain" insert(:user, ap_id: actor, featured_address: "https://example.com/users/lain/collections/featured" ) Tesla.Mock.mock(fn %{ method: :get, url: ^object_url } -> %Tesla.Env{ status: 200, body: status, headers: [{"content-type", "application/activity+json"}] } %{ method: :get, url: ^actor } -> %Tesla.Env{ status: 200, body: user, headers: [{"content-type", "application/activity+json"}] } %{method: :get, url: "https://example.com/users/lain/collections/featured"} -> %Tesla.Env{ status: 200, body: "test/fixtures/users_mock/masto_featured.json" |> File.read!() |> String.replace("{{domain}}", "example.com") |> String.replace("{{nickname}}", "lain"), headers: [{"content-type", "application/activity+json"}] } end) data = %{ "id" => "https://example.com/objects/d61d6733-e256-4fe1-ab13-1e369789423f", "actor" => actor, "object" => object_url, "target" => "https://example.com/users/lain/collections/featured", "type" => "Add", "to" => [Pleroma.Constants.as_public()] } assert "ok" == conn |> assign(:valid_signature, true) |> put_req_header("signature", "keyId=\"#{actor}/main-key\"") |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) |> json_response(200) ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) assert Activity.get_by_ap_id(data["id"]) user = User.get_cached_by_ap_id(data["actor"]) assert user.pinned_objects[data["object"]] data = %{ "id" => "https://example.com/objects/d61d6733-e256-4fe1-ab13-1e369789423d", "actor" => actor, "object" => object_url, "target" => "https://example.com/users/lain/collections/featured", "type" => "Remove", "to" => [Pleroma.Constants.as_public()] } assert "ok" == conn |> assign(:valid_signature, true) |> put_req_header("signature", "keyId=\"#{actor}/main-key\"") |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) |> json_response(200) ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) user = refresh_record(user) refute user.pinned_objects[data["object"]] end test "mastodon pin/unpin", %{conn: conn} do status_id = "105786274556060421" status = File.read!("test/fixtures/statuses/masto-note.json") |> String.replace("{{nickname}}", "lain") |> String.replace("{{status_id}}", status_id) status_url = "https://example.com/users/lain/statuses/#{status_id}" replies_url = status_url <> "/replies?only_other_accounts=true&page=true" user = File.read!("test/fixtures/users_mock/user.json") |> String.replace("{{nickname}}", "lain") actor = "https://example.com/users/lain" sender = insert(:user, ap_id: actor, featured_address: "https://example.com/users/lain/collections/featured" ) |> with_signing_key() Tesla.Mock.mock(fn %{ method: :get, url: ^status_url } -> %Tesla.Env{ status: 200, body: status, headers: [{"content-type", "application/activity+json"}] } %{ method: :get, url: ^actor } -> %Tesla.Env{ status: 200, body: user, headers: [{"content-type", "application/activity+json"}] } %{method: :get, url: "https://example.com/users/lain/collections/featured"} -> %Tesla.Env{ status: 200, body: "test/fixtures/users_mock/masto_featured.json" |> File.read!() |> String.replace("{{domain}}", "example.com") |> String.replace("{{nickname}}", "lain"), headers: [{"content-type", "application/activity+json"}] } %{ method: :get, url: ^replies_url } -> %Tesla.Env{ status: 404, body: "", headers: [{"content-type", "application/activity+json"}] } end) data = %{ "@context" => "https://www.w3.org/ns/activitystreams", "actor" => actor, "object" => status_url, "target" => "https://example.com/users/lain/collections/featured", "type" => "Add" } assert "ok" == conn |> assign(:valid_signature, true) |> put_req_header("signature", "keyId=\"#{sender.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) |> json_response(200) ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) assert Activity.get_by_object_ap_id_with_object(data["object"]) user = User.get_cached_by_ap_id(data["actor"]) assert user.pinned_objects[data["object"]] data = %{ "actor" => actor, "object" => status_url, "target" => "https://example.com/users/lain/collections/featured", "type" => "Remove" } assert "ok" == conn |> assign(:valid_signature, true) |> put_req_header("signature", "keyId=\"#{actor}/main-key\"") |> put_req_header("content-type", "application/activity+json") |> post("/inbox", data) |> json_response(200) ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) assert Activity.get_by_object_ap_id_with_object(data["object"]) user = refresh_record(user) refute user.pinned_objects[data["object"]] end end describe "/users/:nickname/inbox" do setup do data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!() [data: data] end test "it inserts an incoming activity into the database", %{conn: conn, data: data} do user = insert(:user) data = data |> Map.put("bcc", [user.ap_id]) |> Kernel.put_in(["object", "bcc"], [user.ap_id]) conn = conn |> assign(:valid_signature, true) |> put_req_header("signature", "keyId=\"#{data["actor"]}/main-key\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) assert "ok" == json_response(conn, 200) ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) assert Activity.get_by_ap_id(data["id"]) end test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do user = insert(:user) data = data |> Map.put("to", user.ap_id) |> Map.put("cc", []) |> Kernel.put_in(["object", "to"], user.ap_id) |> Kernel.put_in(["object", "cc"], []) conn = conn |> assign(:valid_signature, true) |> put_req_header("signature", "keyId=\"#{data["actor"]}/main-key\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) assert "ok" == json_response(conn, 200) ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) assert Activity.get_by_ap_id(data["id"]) end test "it accepts messages with cc as string instead of array", %{conn: conn, data: data} do user = insert(:user) data = data |> Map.put("to", []) |> Map.put("cc", user.ap_id) |> Kernel.put_in(["object", "to"], []) |> Kernel.put_in(["object", "cc"], user.ap_id) conn = conn |> assign(:valid_signature, true) |> put_req_header("signature", "keyId=\"#{data["actor"]}/main-key\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) assert "ok" == json_response(conn, 200) ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) %Activity{} = activity = Activity.get_by_ap_id(data["id"]) assert user.ap_id in activity.recipients end test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do user = insert(:user) data = data |> Map.put("to", []) |> Map.put("cc", []) |> Map.put("bcc", user.ap_id) |> Kernel.put_in(["object", "to"], []) |> Kernel.put_in(["object", "cc"], []) |> Kernel.put_in(["object", "bcc"], user.ap_id) conn = conn |> assign(:valid_signature, true) |> put_req_header("signature", "keyId=\"#{data["actor"]}/main-key\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) assert "ok" == json_response(conn, 200) ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) assert Activity.get_by_ap_id(data["id"]) end test "it accepts announces with to as string instead of array", %{conn: conn} do user = insert(:user) {:ok, post} = CommonAPI.post(user, %{status: "hey"}) announcer = insert(:user, local: false) |> with_signing_key() data = %{ "@context" => "https://www.w3.org/ns/activitystreams", "actor" => announcer.ap_id, "id" => "#{announcer.ap_id}/statuses/19512778738411822/activity", "object" => post.data["object"], "to" => "https://www.w3.org/ns/activitystreams#Public", "cc" => [user.ap_id], "type" => "Announce" } conn = conn |> assign(:valid_signature, true) |> put_req_header("signature", "keyId=\"#{announcer.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) assert "ok" == json_response(conn, 200) ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) %Activity{} = activity = Activity.get_by_ap_id(data["id"]) assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients end test "it accepts messages from actors that are followed by the user", %{ conn: conn, data: data } do recipient = insert(:user) actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"}) |> with_signing_key() {:ok, recipient, actor} = User.follow(recipient, actor) object = data["object"] |> Map.put("attributedTo", actor.ap_id) data = data |> Map.put("actor", actor.ap_id) |> Map.put("object", object) conn = conn |> assign(:valid_signature, true) |> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{recipient.nickname}/inbox", data) assert "ok" == json_response(conn, 200) ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) assert Activity.get_by_ap_id(data["id"]) end test "it rejects reads from other users", %{conn: conn} do user = insert(:user) other_user = insert(:user) conn = conn |> assign(:user, other_user) |> put_req_header("accept", "application/activity+json") |> get("/users/#{user.nickname}/inbox") assert json_response(conn, 403) end test "it returns a note activity in a collection", %{conn: conn} do note_activity = insert(:direct_note_activity) note_object = Object.normalize(note_activity, fetch: false) user = User.get_cached_by_ap_id(hd(note_activity.data["to"])) conn = conn |> assign(:user, user) |> put_req_header("accept", "application/activity+json") |> get("/users/#{user.nickname}/inbox?page=true") assert response(conn, 200) =~ note_object.data["content"] end test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do user = insert(:user) |> with_signing_key() data = Map.put(data, "bcc", [user.ap_id]) sender_host = URI.parse(data["actor"]).host Instances.set_consistently_unreachable(sender_host) refute Instances.reachable?(sender_host) conn = conn |> assign(:valid_signature, true) |> put_req_header("signature", "keyId=\"#{user.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/inbox", data) assert "ok" == json_response(conn, 200) assert Instances.reachable?(sender_host) end @tag capture_log: true test "it removes all follower collections but actor's", %{conn: conn} do [actor, recipient] = insert_pair(:user) actor = with_signing_key(actor) to = [ recipient.ap_id, recipient.follower_address, "https://www.w3.org/ns/activitystreams#Public" ] cc = [recipient.follower_address, actor.follower_address] data = %{ "@context" => ["https://www.w3.org/ns/activitystreams"], "type" => "Create", "id" => Utils.generate_activity_id(), "to" => to, "cc" => cc, "actor" => actor.ap_id, "object" => %{ "type" => "Note", "to" => to, "cc" => cc, "content" => "It's a note", "attributedTo" => actor.ap_id, "id" => Utils.generate_object_id() } } conn |> assign(:valid_signature, true) |> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{recipient.nickname}/inbox", data) |> json_response(200) ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) assert activity = Activity.get_by_ap_id(data["id"]) assert activity.id assert actor.follower_address in activity.recipients assert actor.follower_address in activity.data["cc"] refute recipient.follower_address in activity.recipients refute recipient.follower_address in activity.data["cc"] refute recipient.follower_address in activity.data["to"] end test "it requires authentication", %{conn: conn} do user = insert(:user) conn = put_req_header(conn, "accept", "application/activity+json") ret_conn = get(conn, "/users/#{user.nickname}/inbox") assert json_response(ret_conn, 403) ret_conn = conn |> assign(:user, user) |> get("/users/#{user.nickname}/inbox") assert json_response(ret_conn, 200) end @tag capture_log: true test "forwarded report", %{conn: conn} do admin = insert(:user, is_admin: true) actor = insert(:user, local: false) |> with_signing_key() remote_domain = URI.parse(actor.ap_id).host reported_user = insert(:user) note = insert(:note_activity, user: reported_user) data = %{ "@context" => [ "https://www.w3.org/ns/activitystreams", "https://#{remote_domain}/schemas/litepub-0.1.jsonld", %{ "@language" => "und" } ], "actor" => actor.ap_id, "cc" => [ reported_user.ap_id ], "content" => "test", "context" => "context", "id" => "http://#{remote_domain}/activities/02be56cf-35e3-46b4-b2c6-47ae08dfee9e", "nickname" => reported_user.nickname, "object" => [ reported_user.ap_id, %{ "actor" => %{ "actor_type" => "Person", "approval_pending" => false, "avatar" => "", "confirmation_pending" => false, "deactivated" => false, "display_name" => "test user", "id" => reported_user.id, "local" => false, "nickname" => reported_user.nickname, "registration_reason" => nil, "roles" => %{ "admin" => false, "moderator" => false }, "tags" => [], "url" => reported_user.ap_id }, "content" => "", "id" => note.data["id"], "published" => note.data["published"], "type" => "Note" } ], "published" => note.data["published"], "state" => "open", "to" => [], "type" => "Flag" } conn |> assign(:valid_signature, true) |> put_req_header("signature", "keyId=\"#{actor.signing_key.key_id}\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{reported_user.nickname}/inbox", data) |> json_response(200) ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) assert Pleroma.Repo.aggregate(Activity, :count, :id) == 2 ObanHelpers.perform_all() Swoosh.TestAssertions.assert_email_sent( to: {admin.name, admin.email}, html_body: ~r/Reported Account:/i ) end @tag capture_log: true test "forwarded report from mastodon", %{conn: conn} do admin = insert(:user, is_admin: true) actor = insert(:user, local: false) remote_domain = URI.parse(actor.ap_id).host remote_actor = "https://#{remote_domain}/actor" [reported_user, another] = insert_list(2, :user) note = insert(:note_activity, user: reported_user) Pleroma.Web.CommonAPI.favorite(another, note.id) mock_json_body = "test/fixtures/mastodon/application_actor.json" |> File.read!() |> String.replace("{{DOMAIN}}", remote_domain) Tesla.Mock.mock(fn %{url: ^remote_actor} -> %Tesla.Env{ status: 200, body: mock_json_body, headers: [{"content-type", "application/activity+json"}] } end) data = %{ "@context" => "https://www.w3.org/ns/activitystreams", "actor" => remote_actor, "content" => "test report", "id" => "https://#{remote_domain}/e3b12fd1-948c-446e-b93b-a5e67edbe1d8", "object" => [ reported_user.ap_id, note.data["object"] ], "type" => "Flag" } conn |> assign(:valid_signature, true) |> put_req_header("signature", "keyId=\"#{remote_actor}/main-key\"") |> put_req_header("content-type", "application/activity+json") |> post("/users/#{reported_user.nickname}/inbox", data) |> json_response(200) ObanHelpers.perform(all_enqueued(worker: ReceiverWorker)) flag_activity = "Flag" |> Pleroma.Activity.Queries.by_type() |> Pleroma.Repo.one() reported_user_ap_id = reported_user.ap_id [^reported_user_ap_id, flag_data] = flag_activity.data["object"] Enum.each(~w(actor content id published type), &Map.has_key?(flag_data, &1)) ObanHelpers.perform_all() Swoosh.TestAssertions.assert_email_sent( to: {admin.name, admin.email}, html_body: ~r/#{note.data["object"]}/i ) end end describe "GET /users/:nickname/outbox" do test "it paginates correctly", %{conn: conn} do user = insert(:user) conn = assign(conn, :user, user) outbox_endpoint = user.ap_id <> "/outbox" _posts = for i <- 0..25 do {:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"}) activity end result = conn |> put_req_header("accept", "application/activity+json") |> get(outbox_endpoint <> "?page=true") |> json_response(200) result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end) assert length(result["orderedItems"]) == 20 assert length(result_ids) == 20 assert result["next"] assert String.starts_with?(result["next"], outbox_endpoint) result_next = conn |> put_req_header("accept", "application/activity+json") |> get(result["next"]) |> json_response(200) result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end) assert length(result_next["orderedItems"]) == 6 assert length(result_next_ids) == 6 refute Enum.find(result_next_ids, fn x -> x in result_ids end) refute Enum.find(result_ids, fn x -> x in result_next_ids end) assert String.starts_with?(result["id"], outbox_endpoint) result_next_again = conn |> put_req_header("accept", "application/activity+json") |> get(result_next["id"]) |> json_response(200) assert result_next == result_next_again end test "it returns 200 even if there're no activities", %{conn: conn} do user = insert(:user) outbox_endpoint = user.ap_id <> "/outbox" conn = conn |> assign(:user, user) |> put_req_header("accept", "application/activity+json") |> get(outbox_endpoint) result = json_response(conn, 200) assert outbox_endpoint == result["id"] end test "it returns a local note activity when authenticated as local user", %{conn: conn} do user = insert(:user) reader = insert(:user) {:ok, note_activity} = CommonAPI.post(user, %{status: "mew mew", visibility: "local"}) ap_id = note_activity.data["id"] resp = conn |> assign(:user, reader) |> put_req_header("accept", "application/activity+json") |> get("/users/#{user.nickname}/outbox?page=true") |> json_response(200) assert %{"orderedItems" => [%{"id" => ^ap_id}]} = resp end test "it does not return a local note activity when unauthenticated", %{conn: conn} do user = insert(:user) {:ok, _note_activity} = CommonAPI.post(user, %{status: "mew mew", visibility: "local"}) resp = conn |> put_req_header("accept", "application/activity+json") |> get("/users/#{user.nickname}/outbox?page=true") |> json_response(200) assert %{"orderedItems" => []} = resp end test "it returns a note activity in a collection", %{conn: conn} do note_activity = insert(:note_activity) note_object = Object.normalize(note_activity, fetch: false) user = User.get_cached_by_ap_id(note_activity.data["actor"]) conn = conn |> assign(:user, user) |> put_req_header("accept", "application/activity+json") |> get("/users/#{user.nickname}/outbox?page=true") assert response(conn, 200) =~ note_object.data["content"] end test "it returns an announce activity in a collection", %{conn: conn} do announce_activity = insert(:announce_activity) user = User.get_cached_by_ap_id(announce_activity.data["actor"]) conn = conn |> assign(:user, user) |> put_req_header("accept", "application/activity+json") |> get("/users/#{user.nickname}/outbox?page=true") assert response(conn, 200) =~ announce_activity.data["object"] end test "It returns poll Answers when authenticated", %{conn: conn} do poller = insert(:user) voter = insert(:user) {:ok, activity} = CommonAPI.post(poller, %{ status: "suya...", poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10} }) assert question = Object.normalize(activity, fetch: false) {:ok, [activity], _object} = CommonAPI.vote(voter, question, [1]) assert outbox_get = conn |> assign(:user, voter) |> put_req_header("accept", "application/activity+json") |> get(voter.ap_id <> "/outbox?page=true") |> json_response(200) assert [answer_outbox] = outbox_get["orderedItems"] assert answer_outbox["id"] == activity.data["id"] end end describe "/relay/followers" do test "it returns relay followers", %{conn: conn} do relay_actor = Relay.get_actor() user = insert(:user) User.follow(user, relay_actor) result = conn |> get("/relay/followers") |> json_response(200) assert result["first"]["orderedItems"] == [user.ap_id] end test "on non-federating instance, it returns 404", %{conn: conn} do clear_config([:instance, :federating], false) user = insert(:user) conn |> assign(:user, user) |> get("/relay/followers") |> json_response(404) end end describe "/relay/following" do test "it returns relay following", %{conn: conn} do result = conn |> get("/relay/following") |> json_response(200) assert result["first"]["orderedItems"] == [] end test "on non-federating instance, it returns 404", %{conn: conn} do clear_config([:instance, :federating], false) user = insert(:user) conn |> assign(:user, user) |> get("/relay/following") |> json_response(404) end end describe "/users/:nickname/followers" do test "it returns the followers in a collection", %{conn: conn} do user = insert(:user) user_two = insert(:user) User.follow(user, user_two) result = conn |> assign(:user, user_two) |> get("/users/#{user_two.nickname}/followers") |> json_response(200) assert result["first"]["orderedItems"] == [user.ap_id] end test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do user = insert(:user) user_two = insert(:user, hide_followers: true) User.follow(user, user_two) result = conn |> assign(:user, user) |> get("/users/#{user_two.nickname}/followers") |> json_response(200) assert is_binary(result["first"]) end test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is from another user", %{conn: conn} do user = insert(:user) other_user = insert(:user, hide_followers: true) result = conn |> assign(:user, user) |> get("/users/#{other_user.nickname}/followers?page=1") assert result.status == 403 assert result.resp_body == "" end test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user", %{conn: conn} do user = insert(:user, hide_followers: true) other_user = insert(:user) {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) result = conn |> assign(:user, user) |> get("/users/#{user.nickname}/followers?page=1") |> json_response(200) assert result["totalItems"] == 1 assert result["orderedItems"] == [other_user.ap_id] end test "it works for more than 10 users", %{conn: conn} do user = insert(:user) Enum.each(1..15, fn _ -> other_user = insert(:user) User.follow(other_user, user) end) result = conn |> assign(:user, user) |> get("/users/#{user.nickname}/followers") |> json_response(200) assert length(result["first"]["orderedItems"]) == 10 assert result["first"]["totalItems"] == 15 assert result["totalItems"] == 15 result = conn |> assign(:user, user) |> get("/users/#{user.nickname}/followers?page=2") |> json_response(200) assert length(result["orderedItems"]) == 5 assert result["totalItems"] == 15 end test "does not require authentication", %{conn: conn} do user = insert(:user) conn |> get("/users/#{user.nickname}/followers") |> json_response(200) end end describe "/users/:nickname/following" do test "it returns the following in a collection", %{conn: conn} do user = insert(:user) user_two = insert(:user) User.follow(user, user_two) result = conn |> assign(:user, user) |> get("/users/#{user.nickname}/following") |> json_response(200) assert result["first"]["orderedItems"] == [user_two.ap_id] end test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do user = insert(:user) user_two = insert(:user, hide_follows: true) User.follow(user, user_two) result = conn |> assign(:user, user) |> get("/users/#{user_two.nickname}/following") |> json_response(200) assert is_binary(result["first"]) end test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is from another user", %{conn: conn} do user = insert(:user) user_two = insert(:user, hide_follows: true) result = conn |> assign(:user, user) |> get("/users/#{user_two.nickname}/following?page=1") assert result.status == 403 assert result.resp_body == "" end test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user", %{conn: conn} do user = insert(:user, hide_follows: true) other_user = insert(:user) {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user) result = conn |> assign(:user, user) |> get("/users/#{user.nickname}/following?page=1") |> json_response(200) assert result["totalItems"] == 1 assert result["orderedItems"] == [other_user.ap_id] end test "it works for more than 10 users", %{conn: conn} do user = insert(:user) Enum.each(1..15, fn _ -> user = User.get_cached_by_id(user.id) other_user = insert(:user) User.follow(user, other_user) end) result = conn |> assign(:user, user) |> get("/users/#{user.nickname}/following") |> json_response(200) assert length(result["first"]["orderedItems"]) == 10 assert result["first"]["totalItems"] == 15 assert result["totalItems"] == 15 result = conn |> assign(:user, user) |> get("/users/#{user.nickname}/following?page=2") |> json_response(200) assert length(result["orderedItems"]) == 5 assert result["totalItems"] == 15 end test "does not require authentication", %{conn: conn} do user = insert(:user) conn |> get("/users/#{user.nickname}/following") |> json_response(200) end end describe "delivery tracking" do test "it tracks a signed object fetch", %{conn: conn} do user = insert(:user, local: false) activity = insert(:note_activity) object = Object.normalize(activity, fetch: false) object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url()) conn |> put_req_header("accept", "application/activity+json") |> assign(:user, user) |> get(object_path) |> json_response(200) assert Delivery.get(object.id, user.id) end test "it tracks a signed activity fetch", %{conn: conn} do user = insert(:user, local: false) activity = insert(:note_activity) object = Object.normalize(activity, fetch: false) activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url()) conn |> put_req_header("accept", "application/activity+json") |> assign(:user, user) |> get(activity_path) |> json_response(200) assert Delivery.get(object.id, user.id) end test "it tracks a signed object fetch when the json is cached", %{conn: conn} do user = insert(:user, local: false) other_user = insert(:user, local: false) activity = insert(:note_activity) object = Object.normalize(activity, fetch: false) object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url()) conn |> put_req_header("accept", "application/activity+json") |> assign(:user, user) |> get(object_path) |> json_response(200) build_conn() |> put_req_header("accept", "application/activity+json") |> assign(:user, other_user) |> get(object_path) |> json_response(200) assert Delivery.get(object.id, user.id) assert Delivery.get(object.id, other_user.id) end test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do user = insert(:user, local: false) other_user = insert(:user, local: false) activity = insert(:note_activity) object = Object.normalize(activity, fetch: false) activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url()) conn |> put_req_header("accept", "application/activity+json") |> assign(:user, user) |> get(activity_path) |> json_response(200) build_conn() |> put_req_header("accept", "application/activity+json") |> assign(:user, other_user) |> get(activity_path) |> json_response(200) assert Delivery.get(object.id, user.id) assert Delivery.get(object.id, other_user.id) end end test "pinned collection", %{conn: conn} do clear_config([:instance, :max_pinned_statuses], 2) user = insert(:user) objects = insert_list(2, :note, user: user) Enum.reduce(objects, user, fn %{data: %{"id" => object_id}}, user -> {:ok, updated} = User.add_pinned_object_id(user, object_id) updated end) %{nickname: nickname, featured_address: featured_address, pinned_objects: pinned_objects} = refresh_record(user) %{"id" => ^featured_address, "orderedItems" => items, "totalItems" => 2} = conn |> get("/users/#{nickname}/collections/featured") |> json_response(200) object_ids = Enum.map(items, & &1["id"]) assert Enum.all?(pinned_objects, fn {obj_id, _} -> obj_id in object_ids end) end end