akkoma/test/pleroma/object/fetcher_test.exs

817 lines
26 KiB
Elixir
Raw Normal View History

# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
2018-12-01 23:03:03 +00:00
defmodule Pleroma.Object.FetcherTest do
use Pleroma.DataCase, async: false
@moduletag :mocked
2018-12-01 23:03:03 +00:00
2019-04-17 09:27:29 +00:00
alias Pleroma.Activity
2022-11-15 17:23:47 +00:00
alias Pleroma.Instances
2019-04-17 09:27:29 +00:00
alias Pleroma.Object
2018-12-01 23:03:03 +00:00
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
2019-06-13 09:34:03 +00:00
mock(fn
%{method: :get, url: "https://mastodon.example.org/users/userisgone"} ->
%Tesla.Env{status: 410}
2019-06-13 10:13:35 +00:00
%{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")
}
%{method: :get, url: "https://octodon.social/users/cwebber/statuses/111647596861000656"} ->
%Tesla.Env{status: 403}
Only allow exact id matches This protects us from falling for obvious spoofs as from the current upload exploit (unfortunately we can’t reasonably do anything about spoofs with exact matches as was possible via emoji and proxy). Such objects being invalid is supported by the spec, sepcifically sections 3.1 and 3.2: https://www.w3.org/TR/activitypub/#obj-id Anonymous objects are not relevant here (they can only exists within parent objects iiuc) and neither is client-to-server or transient objects (as those cannot be fetched in the first place). This leaves us with the requirement for `id` to (a) exist and (b) be a publicly dereferencable URI from the originating server. This alone does not yet demand strict equivalence, but the spec then further explains objects ought to be fetchable _via their ID_. Meaning an object not retrievable via its ID, is invalid. This reading is supported by the fact, e.g. GoToSocial (recently) and Mastodon (for 6+ years) do already implement such strict ID checks, additionally proving this doesn’t cause federation issues in practice. However, apart from canonical IDs there can also be additional display URLs. *omas first redirect those to their canonical location, but *keys and Mastodon directly serve the AP representation without redirects. Mastodon and GTS deal with this in two different ways, but both constitute an effective countermeasure: - Mastodon: Unless it already is a known AP id, two fetches occur. The first fetch just reads the `id` property and then refetches from the id. The last fetch requires the returned id to exactly match the URL the content was fetched from. (This can be optimised by skipping the second fetch if it already matches) https://github.com/mastodon/mastodon/blob/05eda8d19330a9c27c0cf07de19a87edff269057/app/helpers/jsonld_helper.rb#L168 https://github.com/mastodon/mastodon/commit/63f097979990bf5ba9db848b8a253056bad781af - GTS: Only does a single fetch and then checks if _either_ the id _or_ url property (which can be an object) match the original fetch URL. This relies on implementations always including their display URL as "url" if differing from the id. For actors this is true for all investigated implementations, for posts only Mastodon includes an "url", but it is also the only one with a differing display URL. https://github.com/superseriousbusiness/gotosocial/commit/2bafd7daf542d985ee76d9079a30a602cb7be827#diff-943bbb02c8ac74ac5dc5d20807e561dcdfaebdc3b62b10730f643a20ac23c24fR222 Albeit Mastodon’s refetch offers higher compatibility with theoretical implmentations using either multiple different display URL or not denoting any of them as "url" at all, for now we chose to adopt a GTS-like refetch-free approach to avoid additional implementation concerns wrt to whether redirects should be allowed when fetching a canonical AP id and potential for accidentally loosening some checks (e.g. cross-domain refetches) for one of the fetches. This may be reconsidered in the future.
2024-03-16 00:00:19 +00:00
# Spoof: mismatching ids
# Variant 1: Non-exisitng fake id
%{
method: :get,
url:
"https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json"
} ->
%Tesla.Env{
status: 200,
url:
"https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json",
headers: [{"content-type", "application/activity+json"}],
body: spoofed_object_with_ids()
}
%{method: :get, url: "https://patch.cx/objects/spoof"} ->
%Tesla.Env{
status: 404,
url: "https://patch.cx/objects/spoof",
headers: [],
body: "Not found"
}
# Varaint 2: two-stage payload
%{method: :get, url: "https://patch.cx/media/spoof_stage1.json"} ->
%Tesla.Env{
status: 200,
url: "https://patch.cx/media/spoof_stage1.json",
headers: [{"content-type", "application/activity+json"}],
body: spoofed_object_with_ids("https://patch.cx/media/spoof_stage2.json")
}
%{method: :get, url: "https://patch.cx/media/spoof_stage2.json"} ->
%Tesla.Env{
status: 200,
url: "https://patch.cx/media/spoof_stage2.json",
headers: [{"content-type", "application/activity+json"}],
body: spoofed_object_with_ids("https://patch.cx/media/unpredictable.json")
}
# 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,
Only allow exact id matches This protects us from falling for obvious spoofs as from the current upload exploit (unfortunately we can’t reasonably do anything about spoofs with exact matches as was possible via emoji and proxy). Such objects being invalid is supported by the spec, sepcifically sections 3.1 and 3.2: https://www.w3.org/TR/activitypub/#obj-id Anonymous objects are not relevant here (they can only exists within parent objects iiuc) and neither is client-to-server or transient objects (as those cannot be fetched in the first place). This leaves us with the requirement for `id` to (a) exist and (b) be a publicly dereferencable URI from the originating server. This alone does not yet demand strict equivalence, but the spec then further explains objects ought to be fetchable _via their ID_. Meaning an object not retrievable via its ID, is invalid. This reading is supported by the fact, e.g. GoToSocial (recently) and Mastodon (for 6+ years) do already implement such strict ID checks, additionally proving this doesn’t cause federation issues in practice. However, apart from canonical IDs there can also be additional display URLs. *omas first redirect those to their canonical location, but *keys and Mastodon directly serve the AP representation without redirects. Mastodon and GTS deal with this in two different ways, but both constitute an effective countermeasure: - Mastodon: Unless it already is a known AP id, two fetches occur. The first fetch just reads the `id` property and then refetches from the id. The last fetch requires the returned id to exactly match the URL the content was fetched from. (This can be optimised by skipping the second fetch if it already matches) https://github.com/mastodon/mastodon/blob/05eda8d19330a9c27c0cf07de19a87edff269057/app/helpers/jsonld_helper.rb#L168 https://github.com/mastodon/mastodon/commit/63f097979990bf5ba9db848b8a253056bad781af - GTS: Only does a single fetch and then checks if _either_ the id _or_ url property (which can be an object) match the original fetch URL. This relies on implementations always including their display URL as "url" if differing from the id. For actors this is true for all investigated implementations, for posts only Mastodon includes an "url", but it is also the only one with a differing display URL. https://github.com/superseriousbusiness/gotosocial/commit/2bafd7daf542d985ee76d9079a30a602cb7be827#diff-943bbb02c8ac74ac5dc5d20807e561dcdfaebdc3b62b10730f643a20ac23c24fR222 Albeit Mastodon’s refetch offers higher compatibility with theoretical implmentations using either multiple different display URL or not denoting any of them as "url" at all, for now we chose to adopt a GTS-like refetch-free approach to avoid additional implementation concerns wrt to whether redirects should be allowed when fetching a canonical AP id and potential for accidentally loosening some checks (e.g. cross-domain refetches) for one of the fetches. This may be reconsidered in the future.
2024-03-16 00:00:19 +00:00
url: "https://patch.cx/objects/spoof_redirect",
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"
)
}
2019-06-13 09:34:03 +00:00
env ->
apply(HttpRequestMock, :request, [env])
end)
:ok
end
2018-12-01 23:03:03 +00:00
describe "error cases" do
setup do
mock(fn
%{method: :get, url: "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"} ->
%Tesla.Env{
status: 200,
Only allow exact id matches This protects us from falling for obvious spoofs as from the current upload exploit (unfortunately we can’t reasonably do anything about spoofs with exact matches as was possible via emoji and proxy). Such objects being invalid is supported by the spec, sepcifically sections 3.1 and 3.2: https://www.w3.org/TR/activitypub/#obj-id Anonymous objects are not relevant here (they can only exists within parent objects iiuc) and neither is client-to-server or transient objects (as those cannot be fetched in the first place). This leaves us with the requirement for `id` to (a) exist and (b) be a publicly dereferencable URI from the originating server. This alone does not yet demand strict equivalence, but the spec then further explains objects ought to be fetchable _via their ID_. Meaning an object not retrievable via its ID, is invalid. This reading is supported by the fact, e.g. GoToSocial (recently) and Mastodon (for 6+ years) do already implement such strict ID checks, additionally proving this doesn’t cause federation issues in practice. However, apart from canonical IDs there can also be additional display URLs. *omas first redirect those to their canonical location, but *keys and Mastodon directly serve the AP representation without redirects. Mastodon and GTS deal with this in two different ways, but both constitute an effective countermeasure: - Mastodon: Unless it already is a known AP id, two fetches occur. The first fetch just reads the `id` property and then refetches from the id. The last fetch requires the returned id to exactly match the URL the content was fetched from. (This can be optimised by skipping the second fetch if it already matches) https://github.com/mastodon/mastodon/blob/05eda8d19330a9c27c0cf07de19a87edff269057/app/helpers/jsonld_helper.rb#L168 https://github.com/mastodon/mastodon/commit/63f097979990bf5ba9db848b8a253056bad781af - GTS: Only does a single fetch and then checks if _either_ the id _or_ url property (which can be an object) match the original fetch URL. This relies on implementations always including their display URL as "url" if differing from the id. For actors this is true for all investigated implementations, for posts only Mastodon includes an "url", but it is also the only one with a differing display URL. https://github.com/superseriousbusiness/gotosocial/commit/2bafd7daf542d985ee76d9079a30a602cb7be827#diff-943bbb02c8ac74ac5dc5d20807e561dcdfaebdc3b62b10730f643a20ac23c24fR222 Albeit Mastodon’s refetch offers higher compatibility with theoretical implmentations using either multiple different display URL or not denoting any of them as "url" at all, for now we chose to adopt a GTS-like refetch-free approach to avoid additional implementation concerns wrt to whether redirects should be allowed when fetching a canonical AP id and potential for accidentally loosening some checks (e.g. cross-domain refetches) for one of the fetches. This may be reconsidered in the future.
2024-03-16 00:00:19 +00:00
url: "https://social.sakamoto.gq/objects/f20f2497-66d9-4a52-a2e1-1be2a39c32c1",
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
2018-12-01 23:03:03 +00:00
describe "actor origin containment" do
test "it rejects objects with a bogus origin" do
2018-12-01 23:03:03 +00:00
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json")
end
test "it rejects objects when attributedTo is wrong (variant 1)" do
2018-12-01 23:03:03 +00:00
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json")
end
test "it rejects objects when attributedTo is wrong (variant 2)" do
2018-12-01 23:03:03 +00:00
{: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
Only allow exact id matches This protects us from falling for obvious spoofs as from the current upload exploit (unfortunately we can’t reasonably do anything about spoofs with exact matches as was possible via emoji and proxy). Such objects being invalid is supported by the spec, sepcifically sections 3.1 and 3.2: https://www.w3.org/TR/activitypub/#obj-id Anonymous objects are not relevant here (they can only exists within parent objects iiuc) and neither is client-to-server or transient objects (as those cannot be fetched in the first place). This leaves us with the requirement for `id` to (a) exist and (b) be a publicly dereferencable URI from the originating server. This alone does not yet demand strict equivalence, but the spec then further explains objects ought to be fetchable _via their ID_. Meaning an object not retrievable via its ID, is invalid. This reading is supported by the fact, e.g. GoToSocial (recently) and Mastodon (for 6+ years) do already implement such strict ID checks, additionally proving this doesn’t cause federation issues in practice. However, apart from canonical IDs there can also be additional display URLs. *omas first redirect those to their canonical location, but *keys and Mastodon directly serve the AP representation without redirects. Mastodon and GTS deal with this in two different ways, but both constitute an effective countermeasure: - Mastodon: Unless it already is a known AP id, two fetches occur. The first fetch just reads the `id` property and then refetches from the id. The last fetch requires the returned id to exactly match the URL the content was fetched from. (This can be optimised by skipping the second fetch if it already matches) https://github.com/mastodon/mastodon/blob/05eda8d19330a9c27c0cf07de19a87edff269057/app/helpers/jsonld_helper.rb#L168 https://github.com/mastodon/mastodon/commit/63f097979990bf5ba9db848b8a253056bad781af - GTS: Only does a single fetch and then checks if _either_ the id _or_ url property (which can be an object) match the original fetch URL. This relies on implementations always including their display URL as "url" if differing from the id. For actors this is true for all investigated implementations, for posts only Mastodon includes an "url", but it is also the only one with a differing display URL. https://github.com/superseriousbusiness/gotosocial/commit/2bafd7daf542d985ee76d9079a30a602cb7be827#diff-943bbb02c8ac74ac5dc5d20807e561dcdfaebdc3b62b10730f643a20ac23c24fR222 Albeit Mastodon’s refetch offers higher compatibility with theoretical implmentations using either multiple different display URL or not denoting any of them as "url" at all, for now we chose to adopt a GTS-like refetch-free approach to avoid additional implementation concerns wrt to whether redirects should be allowed when fetching a canonical AP id and potential for accidentally loosening some checks (e.g. cross-domain refetches) for one of the fetches. This may be reconsidered in the future.
2024-03-16 00:00:19 +00:00
test "it does not fetch a spoofed object with id different from URL" do
assert {:error, "Object's ActivityPub id/url does not match final fetch URL"} =
Fetcher.fetch_and_contain_remote_object_from_id(
"https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json"
)
assert {:error, "Object's ActivityPub id/url does not match final fetch URL"} =
Fetcher.fetch_and_contain_remote_object_from_id(
"https://patch.cx/media/spoof_stage1.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
test "it does not fetch from localhost" do
assert {:error, "Trying to fetch local resource"} =
Fetcher.fetch_and_contain_remote_object_from_id(
Pleroma.Web.Endpoint.url() <> "/spoof_local"
)
end
end
2018-12-01 23:03:03 +00:00
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"])
2018-12-01 23:03:03 +00:00
{: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
2022-11-15 17:23:47 +00:00
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} =
2022-11-15 17:23:47 +00:00
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
assert Instances.reachable?(id)
end
2018-12-01 23:03:03 +00:00
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
2018-12-01 23:03:03 +00:00
test "all objects with fake directions are rejected by the object fetcher" do
2019-06-13 09:34:03 +00:00
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}} ==
2019-06-13 09:34:03 +00:00
Fetcher.fetch_and_contain_remote_object_from_id(
"https://mastodon.example.org/users/userisgone"
)
2018-12-01 23:03:03 +00:00
end
2019-06-13 10:13:35 +00:00
test "handle HTTP 404 response" do
assert {:error,
{"Object has been deleted", "https://mastodon.example.org/users/userisgone404", 404}} ==
2019-06-13 10:13:35 +00:00
Fetcher.fetch_and_contain_remote_object_from_id(
"https://mastodon.example.org/users/userisgone404"
)
end
2020-06-23 03:30:34 +00:00
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
2018-12-01 23:03:03 +00:00
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,
url: "https://mastodon.social/2",
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,
url: "https://mastodon.social/2",
headers: [
{"content-type",
"application/ld+json; profile=\"https://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,
url: "https://mastodon.social/2",
headers: [{"content-type", "application/json"}],
body: "{}"
}
end)
assert {:error, {:content_type, "application/json"}} =
Fetcher.get_object("https://mastodon.social/2")
end
test "returns the url after redirects" do
Tesla.Mock.mock(fn
%{
method: :get,
url: "https://mastodon.social/5"
} ->
%Tesla.Env{
status: 200,
url: "https://mastodon.social/7",
headers: [{"content-type", "application/activity+json"}],
body: "{}"
}
end)
assert {:ok, "https://mastodon.social/7", "{}"} =
Fetcher.get_object("https://mastodon.social/5")
end
end
2018-12-01 23:03:03 +00:00
end