forked from AkkomaGang/akkoma
Oneric
fee57eb376
This brings it in line with its name and closes an, in practice harmless, verification hole. This was/is the only user of contain_origin making it safe to change the behaviour on actor-less objects. Until now refetched objects did not ensure the new actor matches the domain of the object. We refetch polls occasionally to retrieve up-to-date vote counts. A malicious AP server could have switched out the poll after initial posting with a completely different post attribute to an actor from another server. While we indeed fell for this spoof before the commit, it fortunately seems to have had no ill effect in practice, since the asociated Create activity is not changed. When exposing the actor via our REST API, we read this info from the activity not the object. This at first thought still keeps one avenue for exploit open though: the updated actor can be from our own domain and a third server be instructed to fetch the object from us. However this is foiled by an id mismatch. By necessity of being fetchable and our longstanding same-domain check, the id must still be from the attacker’s server. Even the most barebone authenticity check is able to sus this out.
750 lines
23 KiB
Elixir
750 lines
23 KiB
Elixir
# Pleroma: A lightweight social networking server
|
|
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
defmodule Pleroma.Object.FetcherTest do
|
|
use Pleroma.DataCase, async: false
|
|
@moduletag :mocked
|
|
|
|
alias Pleroma.Activity
|
|
alias Pleroma.Instances
|
|
alias Pleroma.Object
|
|
alias Pleroma.Object.Fetcher
|
|
|
|
import Mock
|
|
import Tesla.Mock
|
|
|
|
defp spoofed_object_with_ids(
|
|
id \\ "https://patch.cx/objects/spoof",
|
|
actor_id \\ "https://patch.cx/users/rin"
|
|
) do
|
|
File.read!("test/fixtures/spoofed-object.json")
|
|
|> Jason.decode!()
|
|
|> Map.put("id", id)
|
|
|> Map.put("actor", actor_id)
|
|
|> Jason.encode!()
|
|
end
|
|
|
|
setup do
|
|
mock(fn
|
|
%{method: :get, url: "https://mastodon.example.org/users/userisgone"} ->
|
|
%Tesla.Env{status: 410}
|
|
|
|
%{method: :get, url: "https://mastodon.example.org/users/userisgone404"} ->
|
|
%Tesla.Env{status: 404}
|
|
|
|
# Spoof: wrong Content-Type
|
|
%{
|
|
method: :get,
|
|
url: "https://patch.cx/objects/spoof_content_type.json"
|
|
} ->
|
|
%Tesla.Env{
|
|
status: 200,
|
|
url: "https://patch.cx/objects/spoof_content_type.json",
|
|
headers: [{"content-type", "application/json"}],
|
|
body: spoofed_object_with_ids("https://patch.cx/objects/spoof_content_type.json")
|
|
}
|
|
|
|
# Spoof: no Content-Type
|
|
%{
|
|
method: :get,
|
|
url: "https://patch.cx/objects/spoof_content_type"
|
|
} ->
|
|
%Tesla.Env{
|
|
status: 200,
|
|
url: "https://patch.cx/objects/spoof_content_type",
|
|
headers: [],
|
|
body: spoofed_object_with_ids("https://patch.cx/objects/spoof_content_type")
|
|
}
|
|
|
|
# Spoof: cross-domain redirect with original domain id
|
|
%{method: :get, url: "https://patch.cx/objects/spoof_media_redirect1"} ->
|
|
%Tesla.Env{
|
|
status: 200,
|
|
url: "https://media.patch.cx/objects/spoof",
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
body: spoofed_object_with_ids("https://patch.cx/objects/spoof_media_redirect1")
|
|
}
|
|
|
|
# Spoof: cross-domain redirect with final domain id
|
|
%{method: :get, url: "https://patch.cx/objects/spoof_media_redirect2"} ->
|
|
%Tesla.Env{
|
|
status: 200,
|
|
url: "https://media.patch.cx/objects/spoof_media_redirect2",
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
body: spoofed_object_with_ids("https://media.patch.cx/objects/spoof_media_redirect2")
|
|
}
|
|
|
|
# No-Spoof: same domain redirect
|
|
%{method: :get, url: "https://patch.cx/objects/spoof_redirect"} ->
|
|
%Tesla.Env{
|
|
status: 200,
|
|
url: "https://patch.cx/objects/spoof",
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
body: spoofed_object_with_ids("https://patch.cx/objects/spoof_redirect")
|
|
}
|
|
|
|
# Spoof: Actor from another domain
|
|
%{method: :get, url: "https://patch.cx/objects/spoof_foreign_actor"} ->
|
|
%Tesla.Env{
|
|
status: 200,
|
|
url: "https://patch.cx/objects/spoof_foreign_actor",
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
body:
|
|
spoofed_object_with_ids(
|
|
"https://patch.cx/objects/spoof_foreign_actor",
|
|
"https://not.patch.cx/users/rin"
|
|
)
|
|
}
|
|
|
|
env ->
|
|
apply(HttpRequestMock, :request, [env])
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
describe "error cases" do
|
|
setup do
|
|
mock(fn
|
|
%{method: :get, url: "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"} ->
|
|
%Tesla.Env{
|
|
status: 200,
|
|
body: File.read!("test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json"),
|
|
headers: HttpRequestMock.activitypub_object_headers()
|
|
}
|
|
|
|
%{method: :get, url: "https://social.sakamoto.gq/users/eal"} ->
|
|
%Tesla.Env{
|
|
status: 200,
|
|
body: File.read!("test/fixtures/fetch_mocks/eal.json"),
|
|
headers: HttpRequestMock.activitypub_object_headers()
|
|
}
|
|
|
|
%{method: :get, url: "https://busshi.moe/users/tuxcrafting/statuses/104410921027210069"} ->
|
|
%Tesla.Env{
|
|
status: 200,
|
|
body: File.read!("test/fixtures/fetch_mocks/104410921027210069.json"),
|
|
headers: HttpRequestMock.activitypub_object_headers()
|
|
}
|
|
|
|
%{method: :get, url: "https://busshi.moe/users/tuxcrafting"} ->
|
|
%Tesla.Env{
|
|
status: 500
|
|
}
|
|
|
|
%{
|
|
method: :get,
|
|
url: "https://stereophonic.space/objects/02997b83-3ea7-4b63-94af-ef3aa2d4ed17"
|
|
} ->
|
|
%Tesla.Env{
|
|
status: 500
|
|
}
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
@tag capture_log: true
|
|
test "it works when fetching the OP actor errors out" do
|
|
# Here we simulate a case where the author of the OP can't be read
|
|
assert {:ok, _} =
|
|
Fetcher.fetch_object_from_id(
|
|
"https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"
|
|
)
|
|
end
|
|
end
|
|
|
|
describe "max thread distance restriction" do
|
|
@ap_id "http://mastodon.example.org/@admin/99541947525187367"
|
|
setup do: clear_config([:instance, :federation_incoming_replies_max_depth])
|
|
|
|
test "it returns thread depth exceeded error if thread depth is exceeded" do
|
|
clear_config([:instance, :federation_incoming_replies_max_depth], 0)
|
|
|
|
assert {:error, "Max thread distance exceeded."} =
|
|
Fetcher.fetch_object_from_id(@ap_id, depth: 1)
|
|
end
|
|
|
|
test "it fetches object if max thread depth is restricted to 0 and depth is not specified" do
|
|
clear_config([:instance, :federation_incoming_replies_max_depth], 0)
|
|
|
|
assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id)
|
|
end
|
|
|
|
test "it fetches object if requested depth does not exceed max thread depth" do
|
|
clear_config([:instance, :federation_incoming_replies_max_depth], 10)
|
|
|
|
assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id, depth: 10)
|
|
end
|
|
end
|
|
|
|
describe "actor origin containment" do
|
|
test "it rejects objects with a bogus origin" do
|
|
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json")
|
|
end
|
|
|
|
test "it rejects objects when attributedTo is wrong (variant 1)" do
|
|
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json")
|
|
end
|
|
|
|
test "it rejects objects when attributedTo is wrong (variant 2)" do
|
|
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity3.json")
|
|
end
|
|
end
|
|
|
|
describe "fetcher security and auth checks" do
|
|
test "it does not fetch a spoofed object without content type" do
|
|
assert {:error, {:content_type, nil}} =
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
"https://patch.cx/objects/spoof_content_type"
|
|
)
|
|
end
|
|
|
|
test "it does not fetch a spoofed object with wrong content type" do
|
|
assert {:error, {:content_type, _}} =
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
"https://patch.cx/objects/spoof_content_type.json"
|
|
)
|
|
end
|
|
|
|
test "it does not fetch an object via cross-domain redirects (initial id)" do
|
|
assert {:error, {:cross_domain_redirect, true}} =
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
"https://patch.cx/objects/spoof_media_redirect1"
|
|
)
|
|
end
|
|
|
|
test "it does not fetch an object via cross-domain redirects (final id)" do
|
|
assert {:error, {:cross_domain_redirect, true}} =
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
"https://patch.cx/objects/spoof_media_redirect2"
|
|
)
|
|
end
|
|
|
|
test "it accepts same-domain redirects" do
|
|
assert {:ok, %{"id" => id} = _object} =
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
"https://patch.cx/objects/spoof_redirect"
|
|
)
|
|
|
|
assert id == "https://patch.cx/objects/spoof_redirect"
|
|
end
|
|
|
|
test "it does not fetch a spoofed object with a foreign actor" do
|
|
assert {:error, "Object containment failed."} =
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
"https://patch.cx/objects/spoof_foreign_actor"
|
|
)
|
|
end
|
|
end
|
|
|
|
describe "fetching an object" do
|
|
test "it fetches an object" do
|
|
{:ok, object} =
|
|
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
|
|
|
|
assert _activity = Activity.get_create_by_object_ap_id(object.data["id"])
|
|
|
|
{:ok, object_again} =
|
|
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
|
|
|
|
assert [attachment] = object.data["attachment"]
|
|
assert is_list(attachment["url"])
|
|
|
|
assert object == object_again
|
|
end
|
|
|
|
test "Return MRF reason when fetched status is rejected by one" do
|
|
clear_config([:mrf_keyword, :reject], ["yeah"])
|
|
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
|
|
|
|
assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} ==
|
|
Fetcher.fetch_object_from_id(
|
|
"http://mastodon.example.org/@admin/99541947525187367"
|
|
)
|
|
end
|
|
|
|
test "does not fetch anything from a rejected instance" do
|
|
clear_config([:mrf_simple, :reject], [{"evil.example.org", "i said so"}])
|
|
|
|
assert {:reject, _} =
|
|
Fetcher.fetch_object_from_id("http://evil.example.org/@admin/99541947525187367")
|
|
end
|
|
|
|
test "does not fetch anything if mrf_simple accept is on" do
|
|
clear_config([:mrf_simple, :accept], [{"mastodon.example.org", "i said so"}])
|
|
clear_config([:mrf_simple, :reject], [])
|
|
|
|
assert {:reject, _} =
|
|
Fetcher.fetch_object_from_id(
|
|
"http://notlisted.example.org/@admin/99541947525187367"
|
|
)
|
|
|
|
assert {:ok, _object} =
|
|
Fetcher.fetch_object_from_id(
|
|
"http://mastodon.example.org/@admin/99541947525187367"
|
|
)
|
|
end
|
|
|
|
test "it resets instance reachability on successful fetch" do
|
|
id = "http://mastodon.example.org/@admin/99541947525187367"
|
|
Instances.set_consistently_unreachable(id)
|
|
refute Instances.reachable?(id)
|
|
|
|
{:ok, _object} =
|
|
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
|
|
|
|
assert Instances.reachable?(id)
|
|
end
|
|
end
|
|
|
|
describe "implementation quirks" do
|
|
test "it can fetch plume articles" do
|
|
{:ok, object} =
|
|
Fetcher.fetch_object_from_id(
|
|
"https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/"
|
|
)
|
|
|
|
assert object
|
|
end
|
|
|
|
test "it can fetch peertube videos" do
|
|
{:ok, object} =
|
|
Fetcher.fetch_object_from_id(
|
|
"https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
|
|
)
|
|
|
|
assert object
|
|
end
|
|
|
|
test "it can fetch Mobilizon events" do
|
|
{:ok, object} =
|
|
Fetcher.fetch_object_from_id(
|
|
"https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"
|
|
)
|
|
|
|
assert object
|
|
end
|
|
|
|
test "it can fetch wedistribute articles" do
|
|
{:ok, object} =
|
|
Fetcher.fetch_object_from_id("https://wedistribute.org/wp-json/pterotype/v1/object/85810")
|
|
|
|
assert object
|
|
end
|
|
|
|
test "all objects with fake directions are rejected by the object fetcher" do
|
|
assert {:error, _} =
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
"https://info.pleroma.site/activity4.json"
|
|
)
|
|
end
|
|
|
|
test "handle HTTP 410 Gone response" do
|
|
assert {:error,
|
|
{"Object has been deleted", "https://mastodon.example.org/users/userisgone", 410}} ==
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
"https://mastodon.example.org/users/userisgone"
|
|
)
|
|
end
|
|
|
|
test "handle HTTP 404 response" do
|
|
assert {:error,
|
|
{"Object has been deleted", "https://mastodon.example.org/users/userisgone404", 404}} ==
|
|
Fetcher.fetch_and_contain_remote_object_from_id(
|
|
"https://mastodon.example.org/users/userisgone404"
|
|
)
|
|
end
|
|
|
|
test "it can fetch pleroma polls with attachments" do
|
|
{:ok, object} =
|
|
Fetcher.fetch_object_from_id("https://patch.cx/objects/tesla_mock/poll_attachment")
|
|
|
|
assert object
|
|
end
|
|
end
|
|
|
|
describe "pruning" do
|
|
test "it can refetch pruned objects" do
|
|
object_id = "http://mastodon.example.org/@admin/99541947525187367"
|
|
|
|
{:ok, object} = Fetcher.fetch_object_from_id(object_id)
|
|
|
|
assert object
|
|
|
|
{:ok, _object} = Object.prune(object)
|
|
|
|
refute Object.get_by_ap_id(object_id)
|
|
|
|
{:ok, %Object{} = object_two} = Fetcher.fetch_object_from_id(object_id)
|
|
|
|
assert object.data["id"] == object_two.data["id"]
|
|
assert object.id != object_two.id
|
|
end
|
|
end
|
|
|
|
describe "signed fetches" do
|
|
setup do: clear_config([:activitypub, :sign_object_fetches])
|
|
|
|
test_with_mock "it signs fetches when configured to do so",
|
|
Pleroma.Signature,
|
|
[:passthrough],
|
|
[] do
|
|
clear_config([:activitypub, :sign_object_fetches], true)
|
|
|
|
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
|
|
|
|
assert called(Pleroma.Signature.sign(:_, :_))
|
|
end
|
|
|
|
test_with_mock "it doesn't sign fetches when not configured to do so",
|
|
Pleroma.Signature,
|
|
[:passthrough],
|
|
[] do
|
|
clear_config([:activitypub, :sign_object_fetches], false)
|
|
|
|
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
|
|
|
|
refute called(Pleroma.Signature.sign(:_, :_))
|
|
end
|
|
end
|
|
|
|
describe "refetching" do
|
|
setup do
|
|
object1 = %{
|
|
"id" => "https://mastodon.social/1",
|
|
"actor" => "https://mastodon.social/users/emelie",
|
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
|
"type" => "Note",
|
|
"content" => "test 1",
|
|
"bcc" => [],
|
|
"bto" => [],
|
|
"cc" => [],
|
|
"to" => [],
|
|
"summary" => ""
|
|
}
|
|
|
|
object2 = %{
|
|
"id" => "https://mastodon.social/2",
|
|
"actor" => "https://mastodon.social/users/emelie",
|
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
|
"type" => "Note",
|
|
"content" => "test 2",
|
|
"bcc" => [],
|
|
"bto" => [],
|
|
"cc" => [],
|
|
"to" => [],
|
|
"summary" => "",
|
|
"formerRepresentations" => %{
|
|
"type" => "OrderedCollection",
|
|
"orderedItems" => [
|
|
%{
|
|
"type" => "Note",
|
|
"content" => "orig 2",
|
|
"actor" => "https://mastodon.social/users/emelie",
|
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
|
"bcc" => [],
|
|
"bto" => [],
|
|
"cc" => [],
|
|
"to" => [],
|
|
"summary" => ""
|
|
}
|
|
],
|
|
"totalItems" => 1
|
|
}
|
|
}
|
|
|
|
mock(fn
|
|
%{
|
|
method: :get,
|
|
url: "https://mastodon.social/1"
|
|
} ->
|
|
%Tesla.Env{
|
|
status: 200,
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
body: Jason.encode!(object1)
|
|
}
|
|
|
|
%{
|
|
method: :get,
|
|
url: "https://mastodon.social/2"
|
|
} ->
|
|
%Tesla.Env{
|
|
status: 200,
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
body: Jason.encode!(object2)
|
|
}
|
|
|
|
%{
|
|
method: :get,
|
|
url: "https://mastodon.social/users/emelie/collections/featured"
|
|
} ->
|
|
%Tesla.Env{
|
|
status: 200,
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
body:
|
|
Jason.encode!(%{
|
|
"id" => "https://mastodon.social/users/emelie/collections/featured",
|
|
"type" => "OrderedCollection",
|
|
"actor" => "https://mastodon.social/users/emelie",
|
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
|
"orderedItems" => [],
|
|
"totalItems" => 0
|
|
})
|
|
}
|
|
|
|
env ->
|
|
apply(HttpRequestMock, :request, [env])
|
|
end)
|
|
|
|
%{object1: object1, object2: object2}
|
|
end
|
|
|
|
test "it keeps formerRepresentations if remote does not have this attr", %{object1: object1} do
|
|
full_object1 =
|
|
object1
|
|
|> Map.merge(%{
|
|
"formerRepresentations" => %{
|
|
"type" => "OrderedCollection",
|
|
"orderedItems" => [
|
|
%{
|
|
"type" => "Note",
|
|
"content" => "orig 2",
|
|
"actor" => "https://mastodon.social/users/emelie",
|
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
|
"bcc" => [],
|
|
"bto" => [],
|
|
"cc" => [],
|
|
"to" => [],
|
|
"summary" => ""
|
|
}
|
|
],
|
|
"totalItems" => 1
|
|
}
|
|
})
|
|
|
|
{:ok, o} = Object.create(full_object1)
|
|
|
|
assert {:ok, refetched} = Fetcher.refetch_object(o)
|
|
|
|
assert %{"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}} =
|
|
refetched.data
|
|
end
|
|
|
|
test "it uses formerRepresentations from remote if possible", %{object2: object2} do
|
|
{:ok, o} = Object.create(object2)
|
|
|
|
assert {:ok, refetched} = Fetcher.refetch_object(o)
|
|
|
|
assert %{"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}} =
|
|
refetched.data
|
|
end
|
|
|
|
test "it replaces formerRepresentations with the one from remote", %{object2: object2} do
|
|
full_object2 =
|
|
object2
|
|
|> Map.merge(%{
|
|
"content" => "mew mew #def",
|
|
"formerRepresentations" => %{
|
|
"type" => "OrderedCollection",
|
|
"orderedItems" => [
|
|
%{"type" => "Note", "content" => "mew mew 2"}
|
|
],
|
|
"totalItems" => 1
|
|
}
|
|
})
|
|
|
|
{:ok, o} = Object.create(full_object2)
|
|
|
|
assert {:ok, refetched} = Fetcher.refetch_object(o)
|
|
|
|
assert %{
|
|
"content" => "test 2",
|
|
"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}
|
|
} = refetched.data
|
|
end
|
|
|
|
test "it adds to formerRepresentations if the remote does not have one and the object has changed",
|
|
%{object1: object1} do
|
|
full_object1 =
|
|
object1
|
|
|> Map.merge(%{
|
|
"content" => "mew mew #def",
|
|
"formerRepresentations" => %{
|
|
"type" => "OrderedCollection",
|
|
"orderedItems" => [
|
|
%{"type" => "Note", "content" => "mew mew 1"}
|
|
],
|
|
"totalItems" => 1
|
|
}
|
|
})
|
|
|
|
{:ok, o} = Object.create(full_object1)
|
|
|
|
assert {:ok, refetched} = Fetcher.refetch_object(o)
|
|
|
|
assert %{
|
|
"content" => "test 1",
|
|
"formerRepresentations" => %{
|
|
"orderedItems" => [
|
|
%{"content" => "mew mew #def"},
|
|
%{"content" => "mew mew 1"}
|
|
],
|
|
"totalItems" => 2
|
|
}
|
|
} = refetched.data
|
|
end
|
|
end
|
|
|
|
describe "fetch with history" do
|
|
setup do
|
|
object2 = %{
|
|
"id" => "https://mastodon.social/2",
|
|
"actor" => "https://mastodon.social/users/emelie",
|
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
|
"type" => "Note",
|
|
"content" => "test 2",
|
|
"bcc" => [],
|
|
"bto" => [],
|
|
"cc" => ["https://mastodon.social/users/emelie/followers"],
|
|
"to" => [],
|
|
"summary" => "",
|
|
"formerRepresentations" => %{
|
|
"type" => "OrderedCollection",
|
|
"orderedItems" => [
|
|
%{
|
|
"type" => "Note",
|
|
"content" => "orig 2",
|
|
"actor" => "https://mastodon.social/users/emelie",
|
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
|
"bcc" => [],
|
|
"bto" => [],
|
|
"cc" => ["https://mastodon.social/users/emelie/followers"],
|
|
"to" => [],
|
|
"summary" => ""
|
|
}
|
|
],
|
|
"totalItems" => 1
|
|
}
|
|
}
|
|
|
|
mock(fn
|
|
%{
|
|
method: :get,
|
|
url: "https://mastodon.social/2"
|
|
} ->
|
|
%Tesla.Env{
|
|
status: 200,
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
body: Jason.encode!(object2)
|
|
}
|
|
|
|
%{
|
|
method: :get,
|
|
url: "https://mastodon.social/users/emelie/collections/featured"
|
|
} ->
|
|
%Tesla.Env{
|
|
status: 200,
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
body:
|
|
Jason.encode!(%{
|
|
"id" => "https://mastodon.social/users/emelie/collections/featured",
|
|
"type" => "OrderedCollection",
|
|
"actor" => "https://mastodon.social/users/emelie",
|
|
"attributedTo" => "https://mastodon.social/users/emelie",
|
|
"orderedItems" => [],
|
|
"totalItems" => 0
|
|
})
|
|
}
|
|
|
|
env ->
|
|
apply(HttpRequestMock, :request, [env])
|
|
end)
|
|
|
|
%{object2: object2}
|
|
end
|
|
|
|
test "it gets history", %{object2: object2} do
|
|
{:ok, object} = Fetcher.fetch_object_from_id(object2["id"])
|
|
|
|
assert %{
|
|
"formerRepresentations" => %{
|
|
"type" => "OrderedCollection",
|
|
"orderedItems" => [%{}]
|
|
}
|
|
} = object.data
|
|
end
|
|
end
|
|
|
|
describe "get_object/1" do
|
|
test "should return ok if the content type is application/activity+json" do
|
|
Tesla.Mock.mock(fn
|
|
%{
|
|
method: :get,
|
|
url: "https://mastodon.social/2"
|
|
} ->
|
|
%Tesla.Env{
|
|
status: 200,
|
|
headers: [{"content-type", "application/activity+json"}],
|
|
body: "{}"
|
|
}
|
|
end)
|
|
|
|
assert {:ok, "{}"} = Fetcher.get_object("https://mastodon.social/2")
|
|
end
|
|
|
|
test "should return ok if the content type is application/ld+json with a profile" do
|
|
Tesla.Mock.mock(fn
|
|
%{
|
|
method: :get,
|
|
url: "https://mastodon.social/2"
|
|
} ->
|
|
%Tesla.Env{
|
|
status: 200,
|
|
headers: [
|
|
{"content-type",
|
|
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""}
|
|
],
|
|
body: "{}"
|
|
}
|
|
end)
|
|
|
|
assert {:ok, "{}"} = Fetcher.get_object("https://mastodon.social/2")
|
|
|
|
Tesla.Mock.mock(fn
|
|
%{
|
|
method: :get,
|
|
url: "https://mastodon.social/2"
|
|
} ->
|
|
%Tesla.Env{
|
|
status: 200,
|
|
headers: [
|
|
{"content-type",
|
|
"application/ld+json; profile=\"http://www.w3.org/ns/activitystreams\""}
|
|
],
|
|
body: "{}"
|
|
}
|
|
end)
|
|
|
|
assert {:ok, "{}"} = Fetcher.get_object("https://mastodon.social/2")
|
|
end
|
|
|
|
test "should not return ok with other content types" do
|
|
Tesla.Mock.mock(fn
|
|
%{
|
|
method: :get,
|
|
url: "https://mastodon.social/2"
|
|
} ->
|
|
%Tesla.Env{
|
|
status: 200,
|
|
headers: [{"content-type", "application/json"}],
|
|
body: "{}"
|
|
}
|
|
end)
|
|
|
|
assert {:error, {:content_type, "application/json"}} =
|
|
Fetcher.get_object("https://mastodon.social/2")
|
|
end
|
|
end
|
|
end
|