Pleroma/test/web/activity_pub/activity_pub_controller_test.exs

1570 lines
46 KiB
Elixir

# 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.ActivityPubControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Activity
alias Pleroma.Config
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)
defp ensure_federating_or_authenticated(conn, url, user) do
Config.put([:instance, :federating], false)
conn
|> get(url)
|> response(403)
conn
|> assign(:user, user)
|> get(url)
|> response(200)
Config.put([:instance, :federating], true)
conn
|> get(url)
|> response(200)
end
describe "/relay" do
setup do: clear_config([:instance, :allow_relay])
test "with the relay active, it returns the relay user", %{conn: conn} do
res =
conn
|> get(activity_pub_path(conn, :relay))
|> json_response(200)
assert res["id"] =~ "/relay"
end
test "with the relay disabled, it returns 404", %{conn: conn} do
Config.put([:instance, :allow_relay], false)
conn
|> get(activity_pub_path(conn, :relay))
|> json_response(404)
end
test "on non-federating instance, it returns 404", %{conn: conn} do
Config.put([:instance, :federating], false)
user = insert(:user)
conn
|> assign(:user, user)
|> get(activity_pub_path(conn, :relay))
|> json_response(404)
end
end
describe "/internal/fetch" do
test "it returns the internal fetch user", %{conn: conn} do
res =
conn
|> get(activity_pub_path(conn, :internal_fetch))
|> json_response(200)
assert res["id"] =~ "/fetch"
end
test "on non-federating instance, it returns 404", %{conn: conn} do
Config.put([:instance, :federating], false)
user = insert(:user)
conn
|> assign(:user, user)
|> get(activity_pub_path(conn, :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
test "it requires authentication if instance is NOT federating", %{
conn: conn
} do
user = insert(:user)
conn =
put_req_header(
conn,
"accept",
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
)
ensure_federating_or_authenticated(conn, "/users/#{user.nickname}.json", user)
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 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 "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 "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
test "it requires authentication if instance is NOT federating", %{
conn: conn
} do
user = insert(:user)
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn = put_req_header(conn, "accept", "application/activity+json")
ensure_federating_or_authenticated(conn, "/objects/#{uuid}", user)
end
end
describe "/activities/:uuid" do
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 "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
test "it requires authentication if instance is NOT federating", %{
conn: conn
} do
user = insert(:user)
activity = insert(:note_activity)
uuid = String.split(activity.data["id"], "/") |> List.last()
conn = put_req_header(conn, "accept", "application/activity+json")
ensure_federating_or_authenticated(conn, "/activities/#{uuid}", user)
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") |> Poison.decode!()
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
assert "ok" == json_response(conn, 200)
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
)
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Map.put("actor", user.ap_id)
|> put_in(["object", "attridbutedTo"], user.ap_id)
conn =
conn
|> assign(:valid_signature, true)
|> 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") |> Poison.decode!()
sender_url = data["actor"]
Instances.set_consistently_unreachable(sender_url)
refute Instances.reachable?(sender_url)
conn =
conn
|> assign(:valid_signature, true)
|> 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
Pleroma.Config.put([: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("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
@tag capture_log: true
test "without valid signature, " <>
"it only accepts Create activities and requires enabled federation",
%{conn: conn} do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!()
conn = put_req_header(conn, "content-type", "application/activity+json")
Config.put([:instance, :federating], false)
conn
|> post("/inbox", data)
|> json_response(403)
conn
|> post("/inbox", non_create_data)
|> json_response(403)
Config.put([:instance, :federating], true)
ret_conn = post(conn, "/inbox", data)
assert "ok" == json_response(ret_conn, 200)
conn
|> post("/inbox", non_create_data)
|> json_response(400)
end
end
describe "/users/:nickname/inbox" do
setup do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
[data: data]
end
test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
user = insert(:user)
data = Map.put(data, "bcc", [user.ap_id])
conn =
conn
|> assign(:valid_signature, true)
|> 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 =
Map.put(data, "to", user.ap_id)
|> Map.delete("cc")
conn =
conn
|> assign(:valid_signature, true)
|> 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 =
Map.put(data, "cc", user.ap_id)
|> Map.delete("to")
conn =
conn
|> assign(:valid_signature, true)
|> 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 =
Map.put(data, "bcc", user.ap_id)
|> Map.delete("to")
|> Map.delete("cc")
conn =
conn
|> assign(:valid_signature, true)
|> 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)
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("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"})
{:ok, recipient} = 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("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)
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)
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("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
assert Instances.reachable?(sender_host)
end
test "it removes all follower collections but actor's", %{conn: conn} do
[actor, recipient] = insert_pair(:user)
data =
File.read!("test/fixtures/activitypub-client-post-activity.json")
|> Poison.decode!()
object = Map.put(data["object"], "attributedTo", actor.ap_id)
data =
data
|> Map.put("id", Utils.generate_object_id())
|> Map.put("actor", actor.ap_id)
|> Map.put("object", object)
|> Map.put("cc", [
recipient.follower_address,
actor.follower_address
])
|> Map.put("to", [
recipient.ap_id,
recipient.follower_address,
"https://www.w3.org/ns/activitystreams#Public"
])
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{recipient.nickname}/inbox", data)
|> json_response(200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
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
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 note activity in a collection", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity)
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 requires authentication if instance is NOT federating", %{
conn: conn
} do
user = insert(:user)
conn = put_req_header(conn, "accept", "application/activity+json")
ensure_federating_or_authenticated(conn, "/users/#{user.nickname}/outbox", user)
end
end
describe "POST /users/:nickname/outbox (C2S)" do
setup do: clear_config([:instance, :limit])
setup do
[
activity: %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"object" => %{"type" => "Note", "content" => "AP C2S test"},
"to" => "https://www.w3.org/ns/activitystreams#Public",
"cc" => []
}
]
end
test "it rejects posts from other users / unauthenticated users", %{
conn: conn,
activity: activity
} do
user = insert(:user)
other_user = insert(:user)
conn = put_req_header(conn, "content-type", "application/activity+json")
conn
|> post("/users/#{user.nickname}/outbox", activity)
|> json_response(403)
conn
|> assign(:user, other_user)
|> post("/users/#{user.nickname}/outbox", activity)
|> json_response(403)
end
test "it inserts an incoming create activity into the database", %{
conn: conn,
activity: activity
} do
user = insert(:user)
result =
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["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)
response =
conn
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", activity)
|> json_response(201)
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
user = insert(:user)
activity = Map.put(activity, "type", "BadType")
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", activity)
assert json_response(conn, 400)
end
test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
data = %{
type: "Delete",
object: %{
id: note_object.data["id"]
}
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
result = json_response(conn, 201)
assert Activity.get_by_ap_id(result["id"])
assert object = Object.get_by_ap_id(note_object.data["id"])
assert object.data["type"] == "Tombstone"
end
test "it rejects delete activity of object from other actor", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity)
user = insert(:user)
data = %{
type: "Delete",
object: %{
id: note_object.data["id"]
}
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 400)
end
test "it increases like count when receiving a like action", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
data = %{
type: "Like",
object: %{
id: note_object.data["id"]
}
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
result = json_response(conn, 201)
assert Activity.get_by_ap_id(result["id"])
assert object = Object.get_by_ap_id(note_object.data["id"])
assert object.data["like_count"] == 1
end
test "it doesn't spreads faulty attributedTo or actor fields", %{
conn: conn,
activity: activity
} do
reimu = insert(:user, nickname: "reimu")
cirno = insert(:user, nickname: "cirno")
assert reimu.ap_id
assert cirno.ap_id
activity =
activity
|> put_in(["object", "actor"], reimu.ap_id)
|> put_in(["object", "attributedTo"], reimu.ap_id)
|> put_in(["actor"], reimu.ap_id)
|> put_in(["attributedTo"], reimu.ap_id)
_reimu_outbox =
conn
|> assign(:user, cirno)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{reimu.nickname}/outbox", activity)
|> json_response(403)
cirno_outbox =
conn
|> assign(:user, cirno)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{cirno.nickname}/outbox", activity)
|> json_response(201)
assert cirno_outbox["attributedTo"] == nil
assert cirno_outbox["actor"] == cirno.ap_id
assert cirno_object = Object.normalize(cirno_outbox["object"])
assert cirno_object.data["actor"] == cirno.ap_id
assert cirno_object.data["attributedTo"] == cirno.ap_id
end
test "Character limitation", %{conn: conn, activity: activity} do
Pleroma.Config.put([:instance, :limit], 5)
user = insert(:user)
result =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", activity)
|> json_response(400)
assert result == "Note is over the character limit"
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
Config.put([: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
Config.put([: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)
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)
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)
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)
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
describe "Additional ActivityPub C2S endpoints" do
test "GET /api/ap/whoami", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> get("/api/ap/whoami")
user = User.get_cached_by_id(user.id)
assert UserView.render("user.json", %{user: user}) == json_response(conn, 200)
conn
|> get("/api/ap/whoami")
|> json_response(403)
end
setup do: clear_config([:media_proxy])
setup do: clear_config([Pleroma.Upload])
test "POST /api/ap/upload_media", %{conn: conn} do
user = insert(:user)
desc = "Description of the image"
image = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
object =
conn
|> assign(:user, user)
|> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
|> json_response(:created)
assert object["name"] == desc
assert object["type"] == "Document"
assert object["actor"] == user.ap_id
assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"]
assert is_binary(object_href)
assert object_mediatype == "image/jpeg"
activity_request = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"object" => %{
"type" => "Note",
"content" => "AP C2S test, attachment",
"attachment" => [object]
},
"to" => "https://www.w3.org/ns/activitystreams#Public",
"cc" => []
}
activity_response =
conn
|> assign(:user, user)
|> post("/users/#{user.nickname}/outbox", activity_request)
|> json_response(:created)
assert activity_response["id"]
assert activity_response["object"]
assert activity_response["actor"] == user.ap_id
assert %Object{data: %{"attachment" => [attachment]}} =
Object.normalize(activity_response["object"])
assert attachment["type"] == "Document"
assert attachment["name"] == desc
assert [
%{
"href" => ^object_href,
"type" => "Link",
"mediaType" => ^object_mediatype
}
] = attachment["url"]
# Fails if unauthenticated
conn
|> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
|> json_response(403)
end
end
end