Merge branch 'develop' into fix/card-skip-quote-link
This commit is contained in:
commit
2c872cf474
808 changed files with 7673 additions and 2171 deletions
2
test/fixtures/bridgy/actor.json
vendored
2
test/fixtures/bridgy/actor.json
vendored
|
|
@ -70,7 +70,7 @@
|
|||
"preferredUsername": "jk.nipponalba.scot",
|
||||
"summary": "",
|
||||
"publicKey": {
|
||||
"id": "jk.nipponalba.scot",
|
||||
"id": "https://fed.brid.gy/jk.nipponalba.scot#key",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdarxwzxnNbJ2hneWOYHkYJowk\npyigQtxlUd0VjgSQHwxU9kWqfbrHBVADyTtcqi/4dAzQd3UnCI1TPNnn4LPZY9PW\noiWd3Zl1/EfLFxO7LU9GS7fcSLQkyj5JNhSlN3I8QPudZbybrgRDVZYooDe1D+52\n5KLGqC2ajrIVOiDRTQIDAQAB\n-----END PUBLIC KEY-----"
|
||||
},
|
||||
"inbox": "https://fed.brid.gy/jk.nipponalba.scot/inbox",
|
||||
|
|
|
|||
BIN
test/fixtures/dji_gps.jpg
vendored
Normal file
BIN
test/fixtures/dji_gps.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
BIN
test/fixtures/strip_location_gps.heic
vendored
Normal file
BIN
test/fixtures/strip_location_gps.heic
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/strip_location_gps.tiff
vendored
Normal file
BIN
test/fixtures/strip_location_gps.tiff
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/strip_location_gps.webp
vendored
Normal file
BIN
test/fixtures/strip_location_gps.webp
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
test/fixtures/strip_location_gps_grid.heic
vendored
Normal file
BIN
test/fixtures/strip_location_gps_grid.heic
vendored
Normal file
Binary file not shown.
|
|
@ -11,7 +11,7 @@
|
|||
"toot": "http://joinmastodon.org/ns#",
|
||||
"Emoji": "toot:Emoji"
|
||||
}],
|
||||
"id": "http://mastodon.example.org/users/admin",
|
||||
"id": "http://mastodon.example.org/users/relay",
|
||||
"type": "Application",
|
||||
"invisible": true,
|
||||
"following": "http://mastodon.example.org/users/admin/following",
|
||||
|
|
@ -24,8 +24,8 @@
|
|||
"url": "http://mastodon.example.org/@admin",
|
||||
"manuallyApprovesFollowers": false,
|
||||
"publicKey": {
|
||||
"id": "http://mastodon.example.org/users/admin#main-key",
|
||||
"owner": "http://mastodon.example.org/users/admin",
|
||||
"id": "http://mastodon.example.org/users/relay#main-key",
|
||||
"owner": "http://mastodon.example.org/users/relay",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtc4Tir+3ADhSNF6VKrtW\nOU32T01w7V0yshmQei38YyiVwVvFu8XOP6ACchkdxbJ+C9mZud8qWaRJKVbFTMUG\nNX4+6Q+FobyuKrwN7CEwhDALZtaN2IPbaPd6uG1B7QhWorrY+yFa8f2TBM3BxnUy\nI4T+bMIZIEYG7KtljCBoQXuTQmGtuffO0UwJksidg2ffCF5Q+K//JfQagJ3UzrR+\nZXbKMJdAw4bCVJYs4Z5EhHYBwQWiXCyMGTd7BGlmMkY6Av7ZqHKC/owp3/0EWDNz\nNqF09Wcpr3y3e8nA10X40MJqp/wR+1xtxp+YGbq/Cj5hZGBG7etFOmIpVBrDOhry\nBwIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"attachment": [{
|
||||
|
|
|
|||
BIN
test/fixtures/xmp_gps.jpg
vendored
Normal file
BIN
test/fixtures/xmp_gps.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
|
|
@ -95,7 +95,7 @@ defmodule Mix.Tasks.Pleroma.InstanceTest do
|
|||
assert generated_config =~ "http: [ip: {127, 0, 0, 1}, port: 4000]"
|
||||
|
||||
assert generated_config =~
|
||||
"filters: [Pleroma.Upload.Filter.Exiftool.StripLocation, Pleroma.Upload.Filter.Exiftool.ReadDescription]"
|
||||
"filters: [Pleroma.Upload.Filter.Exif.StripLocation, Pleroma.Upload.Filter.Exif.ReadDescription]"
|
||||
|
||||
assert File.read!(tmp_path() <> "setup.psql") == generated_setup_psql()
|
||||
assert File.exists?(Path.expand("./test/instance/static/robots.txt"))
|
||||
|
|
|
|||
|
|
@ -13,40 +13,39 @@ defmodule Pleroma.Config.DeprecationWarningsTest do
|
|||
|
||||
describe "filter exiftool" do
|
||||
test "gives warning when still used" do
|
||||
clear_config(
|
||||
[Pleroma.Upload, :filters],
|
||||
[Pleroma.Upload.Filter.Exiftool]
|
||||
)
|
||||
|
||||
assert capture_log(fn -> DeprecationWarnings.check_exiftool_filter() end) =~
|
||||
"""
|
||||
!!!DEPRECATION WARNING!!!
|
||||
Your config is using Exiftool as a filter instead of Exiftool.StripLocation. This should work for now, but you are advised to change to the new configuration to prevent possible issues later:
|
||||
|
||||
```
|
||||
config :pleroma, Pleroma.Upload,
|
||||
filters: [Pleroma.Upload.Filter.Exiftool]
|
||||
```
|
||||
|
||||
Is now
|
||||
|
||||
|
||||
```
|
||||
config :pleroma, Pleroma.Upload,
|
||||
filters: [Pleroma.Upload.Filter.Exiftool.StripLocation]
|
||||
```
|
||||
"""
|
||||
end
|
||||
|
||||
test "changes setting to exiftool strip location" do
|
||||
clear_config(
|
||||
[Pleroma.Upload, :filters],
|
||||
[Pleroma.Upload.Filter.Exiftool, Pleroma.Upload.Filter.Exiftool.ReadDescription]
|
||||
)
|
||||
|
||||
log = capture_log(fn -> DeprecationWarnings.check_exiftool_filter() end)
|
||||
|
||||
assert log =~ "!!!DEPRECATION WARNING!!!"
|
||||
|
||||
assert log =~
|
||||
"* `Pleroma.Upload.Filter.Exiftool` is now `Pleroma.Upload.Filter.Exif.StripLocation`"
|
||||
|
||||
assert log =~
|
||||
"* `Pleroma.Upload.Filter.Exiftool.ReadDescription` is now `Pleroma.Upload.Filter.Exif.ReadDescription`"
|
||||
|
||||
refute log =~ "Exiftool.StripLocation`"
|
||||
end
|
||||
|
||||
test "changes setting to the Exif filters" do
|
||||
clear_config(
|
||||
[Pleroma.Upload, :filters],
|
||||
[
|
||||
Pleroma.Upload.Filter.Dedupe,
|
||||
Pleroma.Upload.Filter.Exiftool,
|
||||
Pleroma.Upload.Filter.Exiftool.StripLocation,
|
||||
Pleroma.Upload.Filter.Exiftool.ReadDescription
|
||||
]
|
||||
)
|
||||
|
||||
expected_config = [
|
||||
Pleroma.Upload.Filter.Exiftool.StripLocation,
|
||||
Pleroma.Upload.Filter.Exiftool.ReadDescription
|
||||
Pleroma.Upload.Filter.Dedupe,
|
||||
Pleroma.Upload.Filter.Exif.StripLocation,
|
||||
Pleroma.Upload.Filter.Exif.ReadDescription
|
||||
]
|
||||
|
||||
capture_log(fn -> DeprecationWarnings.warn() end)
|
||||
|
|
@ -58,8 +57,8 @@ defmodule Pleroma.Config.DeprecationWarningsTest do
|
|||
clear_config(
|
||||
[Pleroma.Upload, :filters],
|
||||
[
|
||||
Pleroma.Upload.Filter.Exiftool.StripLocation,
|
||||
Pleroma.Upload.Filter.Exiftool.ReadDescription
|
||||
Pleroma.Upload.Filter.Exif.StripLocation,
|
||||
Pleroma.Upload.Filter.Exif.ReadDescription
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -96,6 +96,29 @@ defmodule Pleroma.Config.TransferTaskTest do
|
|||
assert Application.get_env(:pleroma, Pleroma.Search.ParadeDB.Repo)[:pool_size] == 2
|
||||
end
|
||||
|
||||
test "skips settings with an unknown module key" do
|
||||
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
|
||||
|
||||
Repo.insert_all("config", [
|
||||
%{
|
||||
group: ":pleroma",
|
||||
key: "Pleroma.Nowhere.Module",
|
||||
value: :erlang.term_to_binary(enabled: true),
|
||||
inserted_at: now,
|
||||
updated_at: now
|
||||
}
|
||||
])
|
||||
|
||||
insert(:config, key: :test_key, value: [live: 2])
|
||||
on_exit(fn -> Application.delete_env(:pleroma, :test_key) end)
|
||||
|
||||
log = capture_log(fn -> assert :ok = TransferTask.load_and_update_env([], false) end)
|
||||
|
||||
assert log =~ "Pleroma.Nowhere.Module"
|
||||
assert Application.get_env(:pleroma, :test_key) == [live: 2]
|
||||
refute Application.get_env(:pleroma, :invalid_atom)
|
||||
end
|
||||
|
||||
describe "pleroma restart" do
|
||||
setup do
|
||||
on_exit(fn ->
|
||||
|
|
|
|||
|
|
@ -50,6 +50,22 @@ defmodule Pleroma.ConfigDBTest do
|
|||
assert config[:pleroma][:instance] == [name: "Pleroma"]
|
||||
end
|
||||
|
||||
test "loads a row with an unknown module key as :invalid_atom" do
|
||||
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
|
||||
|
||||
Repo.insert_all("config", [
|
||||
%{
|
||||
group: ":pleroma",
|
||||
key: "Pleroma.Nowhere.Module",
|
||||
value: :erlang.term_to_binary(enabled: true),
|
||||
inserted_at: now,
|
||||
updated_at: now
|
||||
}
|
||||
])
|
||||
|
||||
assert [%ConfigDB{group: :pleroma, key: :invalid_atom}] = Repo.all(ConfigDB)
|
||||
end
|
||||
|
||||
describe "update_or_create/1" do
|
||||
test "common" do
|
||||
config = insert(:config)
|
||||
|
|
|
|||
|
|
@ -52,4 +52,42 @@ defmodule Pleroma.Emails.MailerTest do
|
|||
subject: "Pleroma test email"
|
||||
)
|
||||
end
|
||||
|
||||
test "keeps certificate checks for HTTP API adapters" do
|
||||
Mailer.deliver(@email)
|
||||
|
||||
assert_received {:email, %Swoosh.Email{private: %{hackney_options: hackney_options}}}
|
||||
|
||||
assert hackney_options[:ssl_options] ==
|
||||
Pleroma.HTTP.TLS.ssl_options()
|
||||
end
|
||||
|
||||
describe "put_mua_cacertfile/1" do
|
||||
test "makes Mua trust the CAStore bundle" do
|
||||
config = Mailer.put_mua_cacertfile(adapter: Swoosh.Adapters.Mua, relay: "mail.example.com")
|
||||
|
||||
assert config[:ssl] == [cacertfile: CAStore.file_path()]
|
||||
assert config[:relay] == "mail.example.com"
|
||||
end
|
||||
|
||||
test "keeps other ssl options" do
|
||||
config = Mailer.put_mua_cacertfile(adapter: Swoosh.Adapters.Mua, ssl: [depth: 3])
|
||||
|
||||
assert config[:ssl] == [cacertfile: CAStore.file_path(), depth: 3]
|
||||
end
|
||||
|
||||
test "keeps a CA that the admin set" do
|
||||
config = [adapter: Swoosh.Adapters.Mua, ssl: [cacertfile: "/etc/ssl/smtp.pem"]]
|
||||
assert Mailer.put_mua_cacertfile(config) == config
|
||||
|
||||
config = [adapter: Swoosh.Adapters.Mua, ssl: [cacerts: []]]
|
||||
assert Mailer.put_mua_cacertfile(config) == config
|
||||
end
|
||||
|
||||
test "leaves other adapters alone" do
|
||||
config = [adapter: Swoosh.Adapters.SMTP, relay: "mail.example.com"]
|
||||
|
||||
assert Mailer.put_mua_cacertfile(config) == config
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -190,4 +190,66 @@ defmodule Pleroma.FilterTest do
|
|||
|
||||
{:error, _} = Filter.update(filter, changes)
|
||||
end
|
||||
|
||||
describe "compose_regex/2" do
|
||||
defp filters(phrases), do: Enum.map(phrases, &%Filter{phrase: &1})
|
||||
|
||||
defp pg_match?(regex, text) do
|
||||
%{rows: [[match]]} = Repo.query!("SELECT $1::text ~* $2", [text, regex])
|
||||
match
|
||||
end
|
||||
|
||||
test "treats phrases as literal text in Postgres" do
|
||||
regex = Filter.compose_regex(filters(["a.b", "(c"]))
|
||||
|
||||
assert pg_match?(regex, "see a.b here")
|
||||
refute pg_match?(regex, "see axb here")
|
||||
assert pg_match?(regex, "see (c here")
|
||||
end
|
||||
|
||||
test "treats phrases as literal text in Elixir" do
|
||||
regex = Filter.compose_regex(filters(["a.b", "(c"]), :re)
|
||||
|
||||
assert Regex.match?(regex, "see a.b here")
|
||||
refute Regex.match?(regex, "see axb here")
|
||||
assert Regex.match?(regex, "see (c here")
|
||||
end
|
||||
|
||||
test "keeps word boundaries for every phrase in Postgres" do
|
||||
regex = Filter.compose_regex(filters(["cat", "dog"]))
|
||||
|
||||
assert pg_match?(regex, "a dog barks")
|
||||
refute pg_match?(regex, "a catalog")
|
||||
refute pg_match?(regex, "a hotdog")
|
||||
end
|
||||
|
||||
test "only adds word boundaries next to word characters" do
|
||||
pg = Filter.compose_regex(filters(["#tag"]))
|
||||
re = Filter.compose_regex(filters(["#tag"]), :re)
|
||||
|
||||
for {text, expected} <- [{"a #tag here", true}, {"a#tag", true}, {"some #tags", false}] do
|
||||
assert pg_match?(pg, text) == expected
|
||||
assert Regex.match?(re, text) == expected
|
||||
end
|
||||
end
|
||||
|
||||
test "matches inside words when whole_word is false" do
|
||||
filters = [%Filter{phrase: "cat", whole_word: false}, %Filter{phrase: "dog"}]
|
||||
pg = Filter.compose_regex(filters)
|
||||
re = Filter.compose_regex(filters, :re)
|
||||
|
||||
for {text, expected} <- [{"a catalog", true}, {"a hotdog", false}, {"a dog", true}] do
|
||||
assert pg_match?(pg, text) == expected
|
||||
assert Regex.match?(re, text) == expected
|
||||
end
|
||||
end
|
||||
|
||||
test "keeps word boundaries for every phrase in Elixir" do
|
||||
regex = Filter.compose_regex(filters(["cat", "dog"]), :re)
|
||||
|
||||
assert Regex.match?(regex, "a dog barks")
|
||||
refute Regex.match?(regex, "a catalog")
|
||||
refute Regex.match?(regex, "a hotdog")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.FormatterTest do
|
|||
alias Pleroma.User
|
||||
use Pleroma.DataCase
|
||||
|
||||
import Mock
|
||||
import Pleroma.Factory
|
||||
|
||||
setup_all do
|
||||
|
|
@ -372,4 +373,99 @@ defmodule Pleroma.FormatterTest do
|
|||
|
||||
assert Formatter.html_escape(text, "text/plain") == expected
|
||||
end
|
||||
|
||||
describe "markdown_to_html/2" do
|
||||
test "keeps formatted link text when stripping email autolinks" do
|
||||
opts = %{breaks: true, strip_email_autolinks: true}
|
||||
|
||||
assert Formatter.markdown_to_html("[**hello**](https://example.org/)", opts) ==
|
||||
~s(<p><a href="https://example.org/"><strong>hello</strong></a></p>)
|
||||
|
||||
assert Formatter.markdown_to_html("[`code`](https://example.org/)", opts) ==
|
||||
~s(<p><a href="https://example.org/"><code class="inline">code</code></a></p>)
|
||||
end
|
||||
|
||||
test "escapes link attribute values" do
|
||||
markdown = ~s|[click](http://example.com/?a=x" onerror="alert(1))|
|
||||
|
||||
# The quotes are percent-encoded, so everything stays inside href.
|
||||
assert Formatter.markdown_to_html(markdown) ==
|
||||
~s|<p><a href="http://example.com/?a=x%22 onerror=%22alert(1)">click</a></p>|
|
||||
end
|
||||
|
||||
test "preserves entities without rendering dangerous raw HTML" do
|
||||
markdown = "safe & sound<script>alert(1)</script>"
|
||||
|
||||
assert Formatter.markdown_to_html(markdown) == "<p>safe & sound</p>"
|
||||
|
||||
assert Formatter.markdown_to_html("<div><script>alert(1)</script>safe</div>") ==
|
||||
"<div>safe</div>"
|
||||
|
||||
assert Formatter.markdown_to_html("<ScRiPt>alert(1)</ScRiPt>") == ""
|
||||
assert Formatter.markdown_to_html("<BR>") == "<BR>"
|
||||
end
|
||||
|
||||
test "escapes dangerous HTML inside code" do
|
||||
markdown = "`<script>&</script>`"
|
||||
|
||||
assert Formatter.markdown_to_html(markdown) ==
|
||||
~s(<p><code class="inline"><script>&amp;</script></code></p>)
|
||||
end
|
||||
|
||||
test "rejects entity-encoded URL delimiters" do
|
||||
markdown = "[click](http://example.com/?a=x"onerror=alert(1))"
|
||||
|
||||
assert Formatter.markdown_to_html(markdown) ==
|
||||
~s|<p><a href="http://example.com/?a=x%22onerror=alert(1)">click</a></p>|
|
||||
end
|
||||
|
||||
test "keeps URLs with encoded quotes complete" do
|
||||
markdown = "[search](https://example.org/?q="cat"&page=2)"
|
||||
|
||||
assert Formatter.markdown_to_html(markdown) ==
|
||||
~s(<p><a href="https://example.org/?q=%22cat%22&page=2">search</a></p>)
|
||||
end
|
||||
|
||||
test "strips email autolinks without changing explicit mailto links" do
|
||||
opts = %{strip_email_autolinks: true}
|
||||
|
||||
assert Formatter.markdown_to_html("<foo@example.com>", opts) ==
|
||||
"<p>foo@example.com</p>"
|
||||
|
||||
assert Formatter.markdown_to_html("[email](mailto:foo@example.com)", opts) ==
|
||||
~s(<p><a href="mailto:foo@example.com">email</a></p>)
|
||||
end
|
||||
|
||||
test "handles malformed Markdown that crashed older EarmarkParser releases" do
|
||||
assert Formatter.markdown_to_html("\\[{:}") == "<p>[{:}</p>"
|
||||
end
|
||||
|
||||
test "handles a malformed Markdown regression corpus" do
|
||||
markdown_samples = [
|
||||
"- hello\n{:.claass .klass}",
|
||||
"[link](\"",
|
||||
"<script",
|
||||
"</style>",
|
||||
"~~~",
|
||||
"<![CDATA[",
|
||||
<<0>>
|
||||
]
|
||||
|
||||
for markdown <- markdown_samples do
|
||||
html = Formatter.markdown_to_html(markdown)
|
||||
|
||||
assert is_binary(html)
|
||||
refute html =~ <<0>>
|
||||
end
|
||||
end
|
||||
|
||||
test "falls back to escaped text when parsing fails" do
|
||||
markdown = "<script>alert(1)</script>\nnext"
|
||||
|
||||
with_mock EarmarkParser, as_ast: fn _text, _opts -> raise "parser failed" end do
|
||||
assert Formatter.markdown_to_html(markdown, %{breaks: true}) ==
|
||||
"<p><script>alert(1)</script><br>next</p>"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ defmodule Pleroma.FrontendTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "admin-fe uses package builds" do
|
||||
assert Pleroma.Config.get([:frontends, :available, "admin-fe", "build_url"]) ==
|
||||
"https://git.pleroma.social/api/packages/pleroma/generic/admin-fe-builds/${ref}/latest.zip"
|
||||
end
|
||||
|
||||
test "it downloads and unzips a known frontend" do
|
||||
clear_config([:frontends, :available], %{
|
||||
"pleroma" => %{
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ defmodule Pleroma.Gun.ConnTest do
|
|||
|
||||
expect(Pleroma.GunMock, :open, fn ~c"proxy.example", 8080, opts ->
|
||||
assert opts.transport == :tcp
|
||||
assert opts.supervise
|
||||
refute Map.has_key?(opts, :tls_opts)
|
||||
{:ok, conn}
|
||||
end)
|
||||
|
|
@ -172,6 +173,67 @@ defmodule Pleroma.Gun.ConnTest do
|
|||
)
|
||||
end
|
||||
|
||||
describe "IPv6-only hosts" do
|
||||
@nxdomain {:error, {:down, {:shutdown, :nxdomain}}}
|
||||
|
||||
test "retries once over IPv6 when the IPv4 lookup finds no address" do
|
||||
ipv4_conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||
ipv6_conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||
|
||||
expect(Pleroma.GunMock, :open, 2, fn
|
||||
~c"v6only.example", 443, %{tcp_opts: tcp_opts} = opts ->
|
||||
assert :inet6 in tcp_opts
|
||||
assert opts.transport == :tls
|
||||
assert opts.tls_opts[:verify] == :verify_peer
|
||||
{:ok, ipv6_conn}
|
||||
|
||||
~c"v6only.example", 443, opts ->
|
||||
refute Map.has_key?(opts, :tcp_opts)
|
||||
# A linked Gun process would take the caller down with the lookup failure.
|
||||
assert opts.supervise
|
||||
{:ok, ipv4_conn}
|
||||
end)
|
||||
|
||||
stub(Pleroma.GunMock, :await_up, fn
|
||||
^ipv4_conn, _ -> @nxdomain
|
||||
^ipv6_conn, _ -> {:ok, :http2}
|
||||
end)
|
||||
|
||||
assert {:ok, ^ipv6_conn, :http2, nil} =
|
||||
Conn.open(URI.parse("https://v6only.example/inbox"), [])
|
||||
|
||||
stop_conn(ipv4_conn)
|
||||
stop_conn(ipv6_conn)
|
||||
end
|
||||
|
||||
test "does not retry IP literals or other errors" do
|
||||
expect(Pleroma.GunMock, :open, 2, fn _, _, _ ->
|
||||
{:ok, spawn(fn -> Process.sleep(:infinity) end)}
|
||||
end)
|
||||
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ -> @nxdomain end)
|
||||
assert @nxdomain = Conn.open(URI.parse("https://192.0.2.1/inbox"), [])
|
||||
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ ->
|
||||
{:error, {:down, {:shutdown, :econnrefused}}}
|
||||
end)
|
||||
|
||||
assert {:error, _} = Conn.open(URI.parse("https://v6only.example/inbox"), [])
|
||||
end
|
||||
|
||||
test "inet6_fallback/3 keeps configured TCP options and skips hosts already on IPv6" do
|
||||
assert {:ok, %{tcp_opts: [:inet6, nodelay: true]}} =
|
||||
Conn.inet6_fallback(@nxdomain, ~c"v6only.example", %{tcp_opts: [nodelay: true]})
|
||||
|
||||
assert {:ok, %{tcp_opts: [:inet6, send_timeout: 15_000, send_timeout_close: true]}} =
|
||||
Conn.inet6_fallback(@nxdomain, ~c"v6only.example", %{})
|
||||
|
||||
assert :error = Conn.inet6_fallback(@nxdomain, ~c"v6only.example", %{tcp_opts: [:inet6]})
|
||||
assert :error = Conn.inet6_fallback(@nxdomain, {192, 0, 2, 1}, %{})
|
||||
assert :error = Conn.inet6_fallback({:error, :timeout}, ~c"v6only.example", %{})
|
||||
end
|
||||
end
|
||||
|
||||
defp stop_conn(conn) do
|
||||
Process.unlink(conn)
|
||||
Process.exit(conn, :kill)
|
||||
|
|
|
|||
|
|
@ -141,6 +141,44 @@ defmodule Pleroma.HTMLTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "link schemes as a browser sees them" do
|
||||
# Browsers remove tabs and newlines in URLs and control characters and
|
||||
# spaces at the start, so these are javascript: links to them.
|
||||
test "removes javascript: links hidden with control characters" do
|
||||
for href <- [
|
||||
"java\tscript:alert(1)",
|
||||
"java	script:alert(1)",
|
||||
"java\nscript:alert(1)",
|
||||
"
javascript:alert(1)",
|
||||
" javascript:alert(1)",
|
||||
"\u0001javascript:alert(1)"
|
||||
],
|
||||
scrubber <- [
|
||||
Pleroma.HTML.Scrubber.Default,
|
||||
Pleroma.HTML.Scrubber.TwitterText,
|
||||
Pleroma.HTML.Scrubber.LinksOnly
|
||||
] do
|
||||
html = HTML.filter_tags(~s(<a href="#{href}">x</a>), scrubber)
|
||||
|
||||
refute html =~ "href", "#{inspect(href)} survived #{inspect(scrubber)}: #{html}"
|
||||
end
|
||||
end
|
||||
|
||||
test "keeps normal links" do
|
||||
for href <- ["https://example.org/a?b=c", "/relative/path", "#fragment"] do
|
||||
assert HTML.filter_tags(~s(<a href="#{href}">x</a>), Pleroma.HTML.Scrubber.Default) ==
|
||||
~s(<a href="#{href}">x</a>)
|
||||
end
|
||||
end
|
||||
|
||||
test "removes images with hidden schemes" do
|
||||
html =
|
||||
HTML.filter_tags(~s|<img src="java\tscript:alert(1)"/>|, Pleroma.HTML.Scrubber.Default)
|
||||
|
||||
refute html =~ "src"
|
||||
end
|
||||
end
|
||||
|
||||
describe "default scrubber" do
|
||||
test "normalizes HTML as expected" do
|
||||
expected = """
|
||||
|
|
|
|||
|
|
@ -50,6 +50,15 @@ defmodule Pleroma.HTTP.AdapterHelper.GunTest do
|
|||
assert Keyword.has_key?(defaults, :b)
|
||||
end
|
||||
|
||||
test "uses Hackney's ssl_options from the config as Gun's tls_opts" do
|
||||
clear_config([:http, :adapter], ssl_options: [versions: [:"tlsv1.3"]])
|
||||
|
||||
opts = Gun.options([], URI.parse("https://example.com"))
|
||||
|
||||
assert opts[:tls_opts] == [versions: [:"tlsv1.3"]]
|
||||
refute Keyword.has_key?(opts, :ssl_options)
|
||||
end
|
||||
|
||||
test "parses string proxy host & port" do
|
||||
clear_config([:http, :proxy_url], "localhost:8123")
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do
|
|||
{:ok, uri: uri}
|
||||
end
|
||||
|
||||
# Hackney 4 shares one HTTP/2 connection per host, and closing a response
|
||||
# stops that connection for every stream on it.
|
||||
test "Hackney only speaks HTTP/1.1" do
|
||||
assert :hackney_util.default_protocols() == [:http1]
|
||||
end
|
||||
|
||||
describe "options/2" do
|
||||
setup do: clear_config([:http, :adapter], a: 1, b: 2)
|
||||
|
||||
|
|
@ -24,6 +30,13 @@ defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do
|
|||
assert opts[:with_body] == true
|
||||
end
|
||||
|
||||
test "does not allow connection options to enable Hackney redirects", %{uri: uri} do
|
||||
opts = Hackney.options([follow_redirect: true, force_redirect: true], uri)
|
||||
|
||||
assert opts[:follow_redirect] == false
|
||||
assert opts[:force_redirect] == false
|
||||
end
|
||||
|
||||
test "add proxy and opts from config", %{uri: uri} do
|
||||
opts = Hackney.options([proxy: "localhost:8123"], uri)
|
||||
|
||||
|
|
@ -32,6 +45,26 @@ defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do
|
|||
assert opts[:proxy] == "localhost:8123"
|
||||
end
|
||||
|
||||
test "gives Hackney a string host for a proxy with an IP address", %{uri: uri} do
|
||||
clear_config([:http, :proxy_url], {:socks5, {127, 0, 0, 1}, 9050})
|
||||
assert Hackney.options([], uri)[:proxy] == {:socks5, ~c"127.0.0.1", 9050}
|
||||
|
||||
clear_config([:http, :proxy_url], {{10, 0, 0, 1}, 8123})
|
||||
assert Hackney.options([], uri)[:proxy] == {~c"10.0.0.1", 8123}
|
||||
|
||||
clear_config([:http, :proxy_url], nil)
|
||||
|
||||
assert Hackney.options([proxy: {:socks5, {0, 0, 0, 0, 0, 0, 0, 1}, 9050}], uri)[:proxy] ==
|
||||
{:socks5, ~c"::1", 9050}
|
||||
end
|
||||
|
||||
# AdminFE stores an empty field as "", and Hackney 4 fails every request with it.
|
||||
test "ignores an empty proxy_url", %{uri: uri} do
|
||||
clear_config([:http, :proxy_url], "")
|
||||
|
||||
refute Keyword.has_key?(Hackney.options([], uri), :proxy)
|
||||
end
|
||||
|
||||
test "respect connection opts and no proxy", %{uri: uri} do
|
||||
opts = Hackney.options([a: 2, b: 1], uri)
|
||||
|
||||
|
|
@ -39,5 +72,20 @@ defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do
|
|||
assert opts[:b] == 1
|
||||
refute Keyword.has_key?(opts, :proxy)
|
||||
end
|
||||
|
||||
test "trusts the CAStore bundle", %{uri: uri} do
|
||||
opts = Hackney.options([], uri)
|
||||
|
||||
assert opts[:ssl_options] == Pleroma.HTTP.TLS.ssl_options()
|
||||
end
|
||||
|
||||
test "keeps the CAStore bundle with configured ssl_options", %{uri: uri} do
|
||||
clear_config([:http, :adapter], ssl_options: [versions: [:"tlsv1.2"]])
|
||||
|
||||
opts = Hackney.options([], uri)
|
||||
|
||||
assert opts[:ssl_options][:versions] == [:"tlsv1.2"]
|
||||
assert opts[:ssl_options][:cacertfile] == CAStore.file_path()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
35
test/pleroma/http/adapter_helper/tesla_adapter_test.exs
Normal file
35
test/pleroma/http/adapter_helper/tesla_adapter_test.exs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.AdapterHelper.TeslaAdapterTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Pleroma.HTTP.AdapterHelper
|
||||
alias Pleroma.HTTP.TLS
|
||||
|
||||
setup do
|
||||
adapter = Application.get_env(:tesla, :adapter)
|
||||
on_exit(fn -> Application.put_env(:tesla, :adapter, adapter) end)
|
||||
end
|
||||
|
||||
test "adds the TLS options to Hackney" do
|
||||
Application.put_env(:tesla, :adapter, Tesla.Adapter.Hackney)
|
||||
|
||||
assert {Tesla.Adapter.Hackney, opts} = AdapterHelper.tesla_adapter()
|
||||
assert opts[:ssl_options] == TLS.ssl_options()
|
||||
assert opts[:proxy_ssl_options] == TLS.ssl_options()
|
||||
end
|
||||
|
||||
test "turns on certificate checks for Gun" do
|
||||
Application.put_env(:tesla, :adapter, Tesla.Adapter.Gun)
|
||||
|
||||
assert AdapterHelper.tesla_adapter() == {Tesla.Adapter.Gun, certificates_verification: true}
|
||||
end
|
||||
|
||||
test "keeps other adapters" do
|
||||
Application.put_env(:tesla, :adapter, {Tesla.Adapter.Finch, name: MyFinch})
|
||||
|
||||
assert AdapterHelper.tesla_adapter() == {Tesla.Adapter.Finch, name: MyFinch}
|
||||
end
|
||||
end
|
||||
|
|
@ -12,6 +12,12 @@ defmodule Pleroma.HTTP.AdapterHelperTest do
|
|||
assert AdapterHelper.format_proxy(nil) == nil
|
||||
end
|
||||
|
||||
test "with an empty string" do
|
||||
assert ExUnit.CaptureLog.capture_log(fn ->
|
||||
assert AdapterHelper.format_proxy("") == nil
|
||||
end) == ""
|
||||
end
|
||||
|
||||
test "with string" do
|
||||
assert AdapterHelper.format_proxy("127.0.0.1:8123") == {{127, 0, 0, 1}, 8123}
|
||||
end
|
||||
|
|
|
|||
195
test/pleroma/http/backoff_test.exs
Normal file
195
test/pleroma/http/backoff_test.exs
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
# Based on Akkoma's test/pleroma/http/backoff_test.exs
|
||||
defmodule Pleroma.HTTP.BackoffTest do
|
||||
use Pleroma.DataCase, async: false
|
||||
|
||||
alias Pleroma.HTTP.Backoff
|
||||
|
||||
@cache :http_backoff_cache
|
||||
@url "https://ratelimited.example.com/users/alice"
|
||||
@host "ratelimited.example.com"
|
||||
|
||||
defp mock_response(status, headers \\ []) do
|
||||
test_pid = self()
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get} ->
|
||||
send(test_pid, :request)
|
||||
%Tesla.Env{status: status, headers: headers, body: ""}
|
||||
end)
|
||||
end
|
||||
|
||||
defp ttl_seconds do
|
||||
{:ok, ttl} = Cachex.ttl(@cache, @host)
|
||||
div(ttl, 1000)
|
||||
end
|
||||
|
||||
defp assert_ttl_about(expected) do
|
||||
ttl = ttl_seconds()
|
||||
assert ttl in (expected - 15)..expected//1, "expected a TTL near #{expected}, got #{ttl}"
|
||||
end
|
||||
|
||||
describe "get/3" do
|
||||
test "returns the response when the host is not backed off" do
|
||||
mock_response(200)
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200}} = Backoff.get(@url)
|
||||
assert {:ok, nil} = Cachex.get(@cache, @host)
|
||||
end
|
||||
|
||||
test "does not send a request while the host is backed off" do
|
||||
Cachex.put(@cache, @host, true)
|
||||
mock_response(200)
|
||||
|
||||
assert {:error, :ratelimit} = Backoff.get(@url)
|
||||
refute_received :request
|
||||
end
|
||||
|
||||
for status <- [429, 503] do
|
||||
test "backs off from the host after a #{status} response" do
|
||||
mock_response(unquote(status))
|
||||
|
||||
assert {:error, :ratelimit} = Backoff.get(@url)
|
||||
assert_received :request
|
||||
assert {:ok, true} = Cachex.get(@cache, @host)
|
||||
|
||||
assert {:error, :ratelimit} = Backoff.get(@url <> "/other")
|
||||
refute_received :request
|
||||
end
|
||||
end
|
||||
|
||||
test "uses a default backoff without headers" do
|
||||
mock_response(429)
|
||||
|
||||
assert {:error, :ratelimit} = Backoff.get(@url)
|
||||
assert_ttl_about(300)
|
||||
end
|
||||
|
||||
test "treats host names case-insensitively" do
|
||||
mock_response(429)
|
||||
assert {:error, :ratelimit} = Backoff.get("https://RateLimited.Example.com/users/alice")
|
||||
assert_received :request
|
||||
|
||||
assert {:error, :ratelimit} = Backoff.get(@url)
|
||||
refute_received :request
|
||||
end
|
||||
|
||||
test "only backs off from the host that answered" do
|
||||
mock_response(429)
|
||||
assert {:error, :ratelimit} = Backoff.get(@url)
|
||||
|
||||
mock_response(200)
|
||||
assert {:ok, _} = Backoff.get("https://other.example.com/users/bob")
|
||||
end
|
||||
|
||||
test "backs off from the host that answered after a redirect" do
|
||||
test_pid = self()
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{url: "https://redirector.example.com/users/alice"} = env ->
|
||||
%{env | status: 302, headers: [{"location", @url}], body: ""}
|
||||
|
||||
%{url: @url} = env ->
|
||||
send(test_pid, :request)
|
||||
%{env | status: 429, headers: [], body: ""}
|
||||
end)
|
||||
|
||||
assert {:error, :ratelimit} = Backoff.get("https://redirector.example.com/users/alice")
|
||||
assert {:ok, true} = Cachex.get(@cache, @host)
|
||||
assert {:ok, nil} = Cachex.get(@cache, "redirector.example.com")
|
||||
end
|
||||
end
|
||||
|
||||
describe "backoff headers" do
|
||||
test "parses Retry-After in seconds" do
|
||||
mock_response(429, [{"retry-after", "600"}])
|
||||
|
||||
assert {:error, :ratelimit} = Backoff.get(@url)
|
||||
assert_ttl_about(600)
|
||||
end
|
||||
|
||||
test "parses Retry-After as an HTTP-date" do
|
||||
date =
|
||||
DateTime.utc_now()
|
||||
|> DateTime.add(600)
|
||||
|> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
|
||||
|
||||
mock_response(503, [{"retry-after", date}])
|
||||
|
||||
assert {:error, :ratelimit} = Backoff.get(@url)
|
||||
assert_ttl_about(600)
|
||||
end
|
||||
|
||||
test "parses Retry-After as an ISO 8601 timestamp" do
|
||||
date = DateTime.utc_now() |> DateTime.add(600) |> DateTime.to_iso8601()
|
||||
mock_response(429, [{"retry-after", date}])
|
||||
|
||||
assert {:error, :ratelimit} = Backoff.get(@url)
|
||||
assert_ttl_about(600)
|
||||
end
|
||||
|
||||
test "parses X-RateLimit-Reset as an ISO 8601 timestamp" do
|
||||
date = DateTime.utc_now() |> DateTime.add(600) |> DateTime.to_iso8601()
|
||||
mock_response(429, [{"x-ratelimit-reset", date}])
|
||||
|
||||
assert {:error, :ratelimit} = Backoff.get(@url)
|
||||
assert_ttl_about(600)
|
||||
end
|
||||
|
||||
test "parses X-RateLimit-Reset as a Unix timestamp" do
|
||||
reset = DateTime.utc_now() |> DateTime.add(600) |> DateTime.to_unix() |> to_string()
|
||||
mock_response(429, [{"x-ratelimit-reset", reset}])
|
||||
|
||||
assert {:error, :ratelimit} = Backoff.get(@url)
|
||||
assert_ttl_about(600)
|
||||
end
|
||||
|
||||
test "parses X-RateLimit-Reset in seconds" do
|
||||
mock_response(429, [{"x-ratelimit-reset", "600"}])
|
||||
|
||||
assert {:error, :ratelimit} = Backoff.get(@url)
|
||||
assert_ttl_about(600)
|
||||
end
|
||||
|
||||
test "matches header names case-insensitively" do
|
||||
mock_response(429, [{"Retry-After", "600"}])
|
||||
|
||||
assert {:error, :ratelimit} = Backoff.get(@url)
|
||||
assert_ttl_about(600)
|
||||
end
|
||||
|
||||
test "prefers Retry-After over X-RateLimit-Reset" do
|
||||
date = DateTime.utc_now() |> DateTime.add(1200) |> DateTime.to_iso8601()
|
||||
mock_response(429, [{"x-ratelimit-reset", date}, {"retry-after", "600"}])
|
||||
|
||||
assert {:error, :ratelimit} = Backoff.get(@url)
|
||||
assert_ttl_about(600)
|
||||
end
|
||||
|
||||
test "falls back to the next header when one is invalid" do
|
||||
date = DateTime.utc_now() |> DateTime.add(600) |> DateTime.to_iso8601()
|
||||
mock_response(429, [{"retry-after", "soon"}, {"x-ratelimit-reset", date}])
|
||||
|
||||
assert {:error, :ratelimit} = Backoff.get(@url)
|
||||
assert_ttl_about(600)
|
||||
end
|
||||
|
||||
for value <- ["soon", "-5", "0", "12.5", "", "Wed, 21 Oct 2015 07:28:00 GMT"] do
|
||||
test "ignores the invalid or past Retry-After value #{inspect(value)}" do
|
||||
mock_response(429, [{"retry-after", unquote(value)}])
|
||||
|
||||
assert {:error, :ratelimit} = Backoff.get(@url)
|
||||
assert_ttl_about(300)
|
||||
end
|
||||
end
|
||||
|
||||
test "caps absurd values at one hour" do
|
||||
mock_response(429, [{"retry-after", "99999999999"}])
|
||||
|
||||
assert {:error, :ratelimit} = Backoff.get(@url)
|
||||
assert_ttl_about(3600)
|
||||
end
|
||||
end
|
||||
end
|
||||
13
test/pleroma/http/default_adapter_test.exs
Normal file
13
test/pleroma/http/default_adapter_test.exs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.DefaultAdapterTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
# The test environment uses Tesla.Mock, and config.exs imports the config
|
||||
# of the current Mix env, so check the shipped default in the file itself.
|
||||
test "Gun is the default HTTP client" do
|
||||
assert File.read!("config/config.exs") =~ ~r/^config :tesla, adapter: Tesla.Adapter.Gun$/m
|
||||
end
|
||||
end
|
||||
199
test/pleroma/http/hackney_conn_leak_test.exs
Normal file
199
test/pleroma/http/hackney_conn_leak_test.exs
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
# Regression guard for the Hackney dependency, not for Pleroma code.
|
||||
#
|
||||
# Hackney 4 leaks a `hackney_conn` gen_statem for an unpooled request that ends
|
||||
# without a reusable socket: a body cut short by the peer, a bodyless 204/304,
|
||||
# or a failed body read. `read_full_body/2` hands back `socket = undefined`, so
|
||||
# the connection goes straight to its `closed` state, bypassing the reuse check;
|
||||
# `closed(enter)` arms a stop timer only for pooled connections, and an unpooled
|
||||
# one is owned by `hackney_conn_sup`, so the owner-DOWN cleanup never fires
|
||||
# either. The process parks forever holding every refc binary it read.
|
||||
#
|
||||
# Pleroma cannot work around this: since 4.0 Hackney ignores `with_body` and
|
||||
# always returns a fully read binary, so a synchronous request never yields a
|
||||
# handle to close. Up to 4.8.2 a truncated read also reported `{:ok, body}`;
|
||||
# since 4.8.3 it is `{:error, {:closed, partial_body}}`.
|
||||
#
|
||||
# Media proxy warming hits this whenever an attachment fetch is cut short and
|
||||
# the outbound proxy is used as a CONNECT tunnel, since tunnels are never
|
||||
# pooled. That is how it was found.
|
||||
#
|
||||
# Fixed upstream by https://github.com/benoitc/hackney/pull/919. Should this
|
||||
# start failing, mix.exs has moved to a Hackney version without the fix.
|
||||
#
|
||||
# https://github.com/benoitc/hackney/issues/918
|
||||
defmodule Pleroma.HTTP.HackneyConnLeakTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Pleroma.HTTP.AdapterHelper.Hackney, as: HackneyAdapterHelper
|
||||
|
||||
@sent_bytes 64 * 1024
|
||||
@announced_bytes 100_000_000
|
||||
|
||||
setup do
|
||||
{:ok, _} = Application.ensure_all_started(:hackney)
|
||||
|
||||
{:ok, server} = start_server()
|
||||
on_exit(fn -> stop_server(server) end)
|
||||
|
||||
{:ok, server: server}
|
||||
end
|
||||
|
||||
test "an unpooled request with a truncated body does not leak a connection process", %{
|
||||
server: server
|
||||
} do
|
||||
before = conn_pids()
|
||||
|
||||
# pool: false takes hackney's connect_direct/4 path, the same unpooled shape
|
||||
# a CONNECT tunnel gets. A pooled connection is reaped by the pool's own
|
||||
# grace timer and would not show the leak.
|
||||
assert {:error, {:closed, body}} = request(server, pool: false)
|
||||
|
||||
# The short read hands back what did arrive.
|
||||
assert byte_size(body) == @sent_bytes
|
||||
|
||||
leaked = wait_for_cleanup(before)
|
||||
|
||||
assert leaked == [],
|
||||
"leaked #{length(leaked)} hackney_conn process(es): " <>
|
||||
inspect(Enum.map(leaked, &describe/1))
|
||||
end
|
||||
|
||||
# A control rather than a guard: a pooled connection is stopped by the pool's
|
||||
# own grace timer both before and after the fix. It is here to show the
|
||||
# unpooled assertion above is measuring the right thing.
|
||||
test "a pooled request with a truncated body does not leak either", %{server: server} do
|
||||
before = conn_pids()
|
||||
|
||||
assert {:error, {:closed, _}} = request(server, pool: :media)
|
||||
|
||||
assert wait_for_cleanup(before) == []
|
||||
end
|
||||
|
||||
defp request(server, extra_opts) do
|
||||
url = "#{server.base_url}/truncated"
|
||||
|
||||
adapter_opts =
|
||||
HackneyAdapterHelper.options([proxy: false] ++ extra_opts, URI.parse(url))
|
||||
|
||||
client = Tesla.client([], Tesla.Adapter.Hackney)
|
||||
|
||||
Tesla.request(client, method: :get, url: url, opts: [adapter: adapter_opts])
|
||||
end
|
||||
|
||||
defp conn_pids do
|
||||
:hackney_conn_sup
|
||||
|> Process.whereis()
|
||||
|> case do
|
||||
nil ->
|
||||
[]
|
||||
|
||||
_ ->
|
||||
for {_, pid, _, _} <- :supervisor.which_children(:hackney_conn_sup), is_pid(pid), do: pid
|
||||
end
|
||||
|> MapSet.new()
|
||||
end
|
||||
|
||||
# Only connections this test created matter; the pool may add unrelated ones.
|
||||
# A pooled connection gets a brief grace window before it stops, so poll
|
||||
# rather than sampling once.
|
||||
defp wait_for_cleanup(before, attempts \\ 40) do
|
||||
leaked =
|
||||
conn_pids()
|
||||
|> Enum.reject(&MapSet.member?(before, &1))
|
||||
|> Enum.filter(&Process.alive?/1)
|
||||
|
||||
cond do
|
||||
leaked == [] -> []
|
||||
attempts == 0 -> leaked
|
||||
true -> Process.sleep(50) && wait_for_cleanup(before, attempts - 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp describe(pid) do
|
||||
state =
|
||||
try do
|
||||
pid |> :sys.get_state(200) |> elem(0)
|
||||
rescue
|
||||
_ -> :unknown
|
||||
catch
|
||||
:exit, _ -> :dead
|
||||
end
|
||||
|
||||
binary =
|
||||
case Process.info(pid, :binary) do
|
||||
{:binary, refs} -> Enum.reduce(refs, 0, fn ref, acc -> acc + elem(ref, 1) end)
|
||||
_ -> 0
|
||||
end
|
||||
|
||||
%{pid: pid, state: state, binary_bytes: binary}
|
||||
end
|
||||
|
||||
# Announces a body far larger than it sends, then closes: what an upstream
|
||||
# aborting mid-stream looks like to the client.
|
||||
defp start_server do
|
||||
{:ok, listener} =
|
||||
:gen_tcp.listen(0, [
|
||||
:binary,
|
||||
active: false,
|
||||
packet: :raw,
|
||||
reuseaddr: true,
|
||||
ip: {127, 0, 0, 1}
|
||||
])
|
||||
|
||||
{:ok, {{127, 0, 0, 1}, port}} = :inet.sockname(listener)
|
||||
{:ok, acceptor} = Task.start(fn -> accept_loop(listener) end)
|
||||
|
||||
{:ok, %{listener: listener, acceptor: acceptor, base_url: "http://127.0.0.1:#{port}"}}
|
||||
end
|
||||
|
||||
# Closing the listener makes the blocked accept/1 return {:error, :closed},
|
||||
# which ends the loop.
|
||||
defp stop_server(%{listener: listener, acceptor: acceptor}) do
|
||||
:ok = :gen_tcp.close(listener)
|
||||
if Process.alive?(acceptor), do: Process.exit(acceptor, :kill)
|
||||
end
|
||||
|
||||
defp accept_loop(listener) do
|
||||
case :gen_tcp.accept(listener) do
|
||||
{:ok, socket} ->
|
||||
serve(socket)
|
||||
accept_loop(listener)
|
||||
|
||||
{:error, _} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp serve(socket) do
|
||||
with {:ok, _data} <- recv_headers(socket) do
|
||||
:gen_tcp.send(socket, [
|
||||
"HTTP/1.1 200 OK\r\n",
|
||||
"Content-Type: application/octet-stream\r\n",
|
||||
"Content-Length: #{@announced_bytes}\r\n\r\n",
|
||||
:binary.copy("x", @sent_bytes)
|
||||
])
|
||||
end
|
||||
|
||||
:gen_tcp.close(socket)
|
||||
end
|
||||
|
||||
defp recv_headers(socket, acc \\ <<>>) do
|
||||
case :gen_tcp.recv(socket, 0, 1_000) do
|
||||
{:ok, data} ->
|
||||
acc = acc <> data
|
||||
|
||||
cond do
|
||||
:binary.match(acc, "\r\n\r\n") != :nomatch -> {:ok, acc}
|
||||
byte_size(acc) > 8_192 -> {:error, :too_large}
|
||||
true -> recv_headers(socket, acc)
|
||||
end
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -19,30 +19,6 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
|
|||
{:ok, tls_server: tls_server, proxy: proxy}
|
||||
end
|
||||
|
||||
test "hackney follow_redirect crashes behind CONNECT proxy on relative redirects", %{
|
||||
tls_server: tls_server,
|
||||
proxy: proxy
|
||||
} do
|
||||
url = "#{tls_server.base_url}/redirect"
|
||||
|
||||
opts = [
|
||||
pool: :media,
|
||||
proxy: proxy.proxy_url,
|
||||
insecure: true,
|
||||
connect_timeout: 1_000,
|
||||
recv_timeout: 1_000,
|
||||
follow_redirect: true,
|
||||
force_redirect: true
|
||||
]
|
||||
|
||||
{pid, ref} = spawn_monitor(fn -> :hackney.request(:get, url, [], <<>>, opts) end)
|
||||
|
||||
assert_receive {:DOWN, ^ref, :process, ^pid, reason}, 5_000
|
||||
|
||||
assert match?({%FunctionClauseError{}, _}, reason) or match?(%FunctionClauseError{}, reason) or
|
||||
match?({:function_clause, _}, reason)
|
||||
end
|
||||
|
||||
test "redirects work via proxy when hackney follow_redirect is disabled", %{
|
||||
tls_server: tls_server,
|
||||
proxy: proxy
|
||||
|
|
@ -52,7 +28,7 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
|
|||
adapter_opts = [
|
||||
pool: :media,
|
||||
proxy: proxy.proxy_url,
|
||||
insecure: true,
|
||||
ssl_options: tls_client_options(tls_server),
|
||||
connect_timeout: 1_000,
|
||||
recv_timeout: 1_000,
|
||||
follow_redirect: false,
|
||||
|
|
@ -75,7 +51,7 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
|
|||
opts = [
|
||||
pool: :media,
|
||||
proxy: proxy.proxy_url,
|
||||
insecure: true,
|
||||
ssl_options: tls_client_options(tls_server),
|
||||
connect_timeout: 1_000,
|
||||
recv_timeout: 1_000,
|
||||
follow_redirect: true
|
||||
|
|
@ -97,7 +73,7 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
|
|||
opts = [
|
||||
pool: :media,
|
||||
proxy: proxy.proxy_url,
|
||||
insecure: true,
|
||||
ssl_options: tls_client_options(tls_server),
|
||||
connect_timeout: 1_000,
|
||||
recv_timeout: 1_000,
|
||||
follow_redirect: true
|
||||
|
|
@ -119,7 +95,7 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
|
|||
opts = [
|
||||
pool: :media,
|
||||
proxy: proxy.proxy_url,
|
||||
insecure: true,
|
||||
ssl_options: tls_client_options(tls_server),
|
||||
connect_timeout: 1_000,
|
||||
recv_timeout: 1_000,
|
||||
follow_redirect: true
|
||||
|
|
@ -131,6 +107,133 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
|
|||
Pleroma.ReverseProxy.Client.Hackney.close(ref)
|
||||
end
|
||||
|
||||
test "hackney reverse proxy cancels timed out asynchronous requests", %{
|
||||
tls_server: tls_server,
|
||||
proxy: proxy
|
||||
} do
|
||||
url = "#{tls_server.base_url}/delayed"
|
||||
|
||||
opts = [
|
||||
pool: :media,
|
||||
proxy: proxy.proxy_url,
|
||||
ssl_options: tls_client_options(tls_server),
|
||||
connect_timeout: 1_000,
|
||||
recv_timeout: 10,
|
||||
follow_redirect: false
|
||||
]
|
||||
|
||||
assert {:error, :timeout} =
|
||||
Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts)
|
||||
|
||||
refute_receive {:hackney_response, _, _}, 150
|
||||
end
|
||||
|
||||
test "hackney reverse proxy normalizes asynchronous connection errors", %{
|
||||
tls_server: tls_server,
|
||||
proxy: proxy
|
||||
} do
|
||||
url = "#{tls_server.base_url}/closed"
|
||||
|
||||
opts = [
|
||||
pool: :media,
|
||||
proxy: proxy.proxy_url,
|
||||
ssl_options: tls_client_options(tls_server),
|
||||
connect_timeout: 1_000,
|
||||
recv_timeout: 1_000,
|
||||
follow_redirect: false
|
||||
]
|
||||
|
||||
assert {:error, :closed} =
|
||||
Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts)
|
||||
end
|
||||
|
||||
# A pooled connection serving a bodyless response is checked back in by hackney
|
||||
# at header time, so stopping that ref afterwards would evict a connection
|
||||
# another request may already hold. These go direct rather than through the
|
||||
# CONNECT proxy, because a proxy tunnel is never pooled.
|
||||
test "hackney reverse proxy marks a pooled bodyless response as already released",
|
||||
%{tls_server: tls_server} do
|
||||
url = "#{tls_server.base_url}/no_content"
|
||||
|
||||
assert {:ok, 204, _headers, client} =
|
||||
Pleroma.ReverseProxy.Client.Hackney.request(
|
||||
:get,
|
||||
url,
|
||||
[],
|
||||
"",
|
||||
direct_opts(tls_server)
|
||||
)
|
||||
|
||||
assert %{released: true} = client
|
||||
assert :done = Pleroma.ReverseProxy.Client.Hackney.stream_body(client)
|
||||
assert :ok = Pleroma.ReverseProxy.Client.Hackney.close(client)
|
||||
end
|
||||
|
||||
test "hackney reverse proxy still owns a response that carries a body",
|
||||
%{tls_server: tls_server} do
|
||||
url = "#{tls_server.base_url}/final"
|
||||
|
||||
assert {:ok, 200, _headers, client} =
|
||||
Pleroma.ReverseProxy.Client.Hackney.request(
|
||||
:get,
|
||||
url,
|
||||
[],
|
||||
"",
|
||||
direct_opts(tls_server)
|
||||
)
|
||||
|
||||
refute Map.has_key?(client, :released)
|
||||
assert "ok" == collect_body(client)
|
||||
|
||||
# Streaming to the end must stop the conn: hackney only parks it, and this
|
||||
# one has no idle timeout and no live owner monitor to reap it.
|
||||
refute Process.alive?(client.ref)
|
||||
end
|
||||
|
||||
# An unpooled connection is never checked in and parks forever with no idle
|
||||
# timeout, so it stays ours to stop even when the response has no body.
|
||||
test "hackney reverse proxy still owns a bodyless response over a proxy tunnel", %{
|
||||
tls_server: tls_server,
|
||||
proxy: proxy
|
||||
} do
|
||||
url = "#{tls_server.base_url}/no_content"
|
||||
|
||||
opts = Keyword.put(direct_opts(tls_server), :proxy, proxy.proxy_url)
|
||||
|
||||
assert {:ok, 204, _headers, client} =
|
||||
Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts)
|
||||
|
||||
refute Map.has_key?(client, :released)
|
||||
Pleroma.ReverseProxy.Client.Hackney.close(client)
|
||||
end
|
||||
|
||||
test "hackney reverse proxy still owns a bodyless response when pooling is off", %{
|
||||
tls_server: tls_server
|
||||
} do
|
||||
url = "#{tls_server.base_url}/no_content"
|
||||
|
||||
opts = Keyword.put(direct_opts(tls_server), :pool, false)
|
||||
|
||||
assert {:ok, 204, _headers, client} =
|
||||
Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts)
|
||||
|
||||
refute Map.has_key?(client, :released)
|
||||
Pleroma.ReverseProxy.Client.Hackney.close(client)
|
||||
end
|
||||
|
||||
# proxy: false so a proxy in the build environment cannot turn these into
|
||||
# tunnelled requests, which are never pooled and would defeat the point.
|
||||
defp direct_opts(tls_server) do
|
||||
[
|
||||
pool: :media,
|
||||
proxy: false,
|
||||
ssl_options: tls_client_options(tls_server),
|
||||
connect_timeout: 1_000,
|
||||
recv_timeout: 1_000,
|
||||
follow_redirect: false
|
||||
]
|
||||
end
|
||||
|
||||
defp collect_body(ref, acc \\ "") do
|
||||
case Pleroma.ReverseProxy.Client.Hackney.stream_body(ref) do
|
||||
:done -> acc
|
||||
|
|
@ -161,7 +264,13 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
|
|||
accept_tls_loop(listener)
|
||||
end)
|
||||
|
||||
{:ok, %{listener: listener, acceptor: acceptor, base_url: "https://127.0.0.1:#{port}"}}
|
||||
{:ok,
|
||||
%{
|
||||
listener: listener,
|
||||
acceptor: acceptor,
|
||||
certfile: certfile,
|
||||
base_url: "https://127.0.0.1:#{port}"
|
||||
}}
|
||||
end
|
||||
|
||||
defp stop_tls_redirect_server(%{listener: listener, acceptor: acceptor}) do
|
||||
|
|
@ -191,6 +300,16 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
|
|||
{:ok, data} <- recv_ssl_headers(ssl_socket),
|
||||
{:ok, path} <- parse_path(data) do
|
||||
case path do
|
||||
"/closed" ->
|
||||
:ok
|
||||
|
||||
"/no_content" ->
|
||||
send_ssl_response(ssl_socket, 204, "No Content", [], "")
|
||||
|
||||
"/delayed" ->
|
||||
Process.sleep(100)
|
||||
send_ssl_response(ssl_socket, 200, "OK", [], "ok")
|
||||
|
||||
"/infinite_redirect" ->
|
||||
send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/infinite_redirect"}], "")
|
||||
|
||||
|
|
@ -210,11 +329,17 @@ defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
|
|||
:ssl.close(ssl_socket)
|
||||
else
|
||||
_ ->
|
||||
_ = :gen_tcp.close(tcp_socket)
|
||||
_ = :ssl.close(tcp_socket)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp tls_client_options(_tls_server) do
|
||||
[server_name_indication: ~c"localhost", verify_fun: {&accept_test_certificate/3, []}]
|
||||
end
|
||||
|
||||
defp accept_test_certificate(_cert, _event, state), do: {:valid, state}
|
||||
|
||||
defp recv_ssl_headers(socket, acc \\ <<>>) do
|
||||
case :ssl.recv(socket, 0, 1_000) do
|
||||
{:ok, data} ->
|
||||
|
|
|
|||
163
test/pleroma/http/hackney_max_body_regression_test.exs
Normal file
163
test/pleroma/http/hackney_max_body_regression_test.exs
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
# Hackney 4 ignores `with_body` and always returns a fully read binary, so
|
||||
# `Tesla.Adapter.Hackney` never reaches the clause that honours `:max_body`.
|
||||
# Hackney 4.9 enforces `max_body` itself while reading (CVE-2026-97200);
|
||||
# Pleroma.Tesla.Middleware.MaxBody stays as the check for every adapter. These
|
||||
# exercise both against the real adapter.
|
||||
defmodule Pleroma.HTTP.HackneyMaxBodyRegressionTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Pleroma.HTTP.AdapterHelper.Hackney, as: HackneyAdapterHelper
|
||||
|
||||
@body_bytes 64 * 1024
|
||||
|
||||
setup do
|
||||
{:ok, _} = Application.ensure_all_started(:hackney)
|
||||
|
||||
{:ok, server} = start_server()
|
||||
on_exit(fn -> stop_server(server) end)
|
||||
|
||||
{:ok, server: server}
|
||||
end
|
||||
|
||||
defp request(url, extra_opts) do
|
||||
adapter_opts =
|
||||
HackneyAdapterHelper.options(
|
||||
[pool: :media, proxy: false] ++ extra_opts,
|
||||
URI.parse(url)
|
||||
)
|
||||
|
||||
client =
|
||||
Tesla.client(
|
||||
[Tesla.Middleware.FollowRedirects, Pleroma.Tesla.Middleware.MaxBody],
|
||||
Tesla.Adapter.Hackney
|
||||
)
|
||||
|
||||
Tesla.request(client, method: :get, url: url, opts: [adapter: adapter_opts])
|
||||
end
|
||||
|
||||
test "a body over max_body is refused", %{server: server} do
|
||||
assert {:error, :body_too_large} =
|
||||
request("#{server.base_url}/big", max_body: 1024)
|
||||
end
|
||||
|
||||
# Without a Content-Length the header pre-check cannot fire, so only a size
|
||||
# check on the body can refuse this.
|
||||
test "a chunked body over max_body is refused on size alone", %{server: server} do
|
||||
assert {:error, :body_too_large} =
|
||||
request("#{server.base_url}/chunked", max_body: 1024)
|
||||
end
|
||||
|
||||
test "a body under max_body is returned", %{server: server} do
|
||||
assert {:ok, %Tesla.Env{status: 200, body: body}} =
|
||||
request("#{server.base_url}/big", max_body: @body_bytes * 2)
|
||||
|
||||
assert byte_size(body) == @body_bytes
|
||||
end
|
||||
|
||||
test "without max_body the body is returned", %{server: server} do
|
||||
assert {:ok, %Tesla.Env{status: 200, body: body}} = request("#{server.base_url}/big", [])
|
||||
assert byte_size(body) == @body_bytes
|
||||
end
|
||||
|
||||
# Hackney 4.0 to 4.8 read the whole body whatever `max_body` said; 4.9
|
||||
# stops reading once the body goes over it (CVE-2026-97200). Should this
|
||||
# start failing, the hackney pin has gone back to a version without the fix.
|
||||
test "the bare adapter enforces max_body", %{server: server} do
|
||||
url = "#{server.base_url}/chunked"
|
||||
|
||||
adapter_opts =
|
||||
HackneyAdapterHelper.options(
|
||||
[pool: :media, proxy: false, max_body: 1024],
|
||||
URI.parse(url)
|
||||
)
|
||||
|
||||
client = Tesla.client([], Tesla.Adapter.Hackney)
|
||||
|
||||
assert {:error, :body_too_large} =
|
||||
Tesla.request(client, method: :get, url: url, opts: [adapter: adapter_opts])
|
||||
end
|
||||
|
||||
defp start_server do
|
||||
{:ok, listener} =
|
||||
:gen_tcp.listen(0, [
|
||||
:binary,
|
||||
active: false,
|
||||
packet: :raw,
|
||||
reuseaddr: true,
|
||||
ip: {127, 0, 0, 1}
|
||||
])
|
||||
|
||||
{:ok, {{127, 0, 0, 1}, port}} = :inet.sockname(listener)
|
||||
{:ok, acceptor} = Task.start(fn -> accept_loop(listener) end)
|
||||
|
||||
{:ok, %{listener: listener, acceptor: acceptor, base_url: "http://127.0.0.1:#{port}"}}
|
||||
end
|
||||
|
||||
# Closing the listener makes the blocked accept/1 return {:error, :closed},
|
||||
# which ends the loop.
|
||||
defp stop_server(%{listener: listener, acceptor: acceptor}) do
|
||||
:ok = :gen_tcp.close(listener)
|
||||
if Process.alive?(acceptor), do: Process.exit(acceptor, :kill)
|
||||
end
|
||||
|
||||
defp accept_loop(listener) do
|
||||
case :gen_tcp.accept(listener) do
|
||||
{:ok, socket} ->
|
||||
serve(socket)
|
||||
accept_loop(listener)
|
||||
|
||||
{:error, _} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp serve(socket) do
|
||||
with {:ok, data} <- recv_headers(socket) do
|
||||
body = :binary.copy("x", @body_bytes)
|
||||
|
||||
if String.contains?(data, "/chunked") do
|
||||
# No Content-Length, so only the body-size check can catch this.
|
||||
:gen_tcp.send(socket, [
|
||||
"HTTP/1.1 200 OK\r\n",
|
||||
"Content-Type: text/plain\r\n",
|
||||
"Transfer-Encoding: chunked\r\n",
|
||||
"Connection: close\r\n\r\n",
|
||||
:erlang.integer_to_list(byte_size(body), 16),
|
||||
"\r\n",
|
||||
body,
|
||||
"\r\n0\r\n\r\n"
|
||||
])
|
||||
else
|
||||
:gen_tcp.send(socket, [
|
||||
"HTTP/1.1 200 OK\r\n",
|
||||
"Content-Type: text/plain\r\n",
|
||||
"Content-Length: #{byte_size(body)}\r\n",
|
||||
"Connection: close\r\n\r\n",
|
||||
body
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
:gen_tcp.close(socket)
|
||||
end
|
||||
|
||||
defp recv_headers(socket, acc \\ <<>>) do
|
||||
case :gen_tcp.recv(socket, 0, 1_000) do
|
||||
{:ok, data} ->
|
||||
acc = acc <> data
|
||||
|
||||
cond do
|
||||
:binary.match(acc, "\r\n\r\n") != :nomatch -> {:ok, acc}
|
||||
byte_size(acc) > 8_192 -> {:error, :too_large}
|
||||
true -> recv_headers(socket, acc)
|
||||
end
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -22,7 +22,7 @@ defmodule Pleroma.HTTP.HackneyRedirectRegressionTest do
|
|||
|
||||
adapter_opts =
|
||||
HackneyAdapterHelper.options(
|
||||
[pool: :media, follow_redirect: false, no_proxy_env: true],
|
||||
[pool: :media, follow_redirect: false, proxy: false],
|
||||
uri
|
||||
)
|
||||
|
||||
|
|
|
|||
70
test/pleroma/http/tls_test.exs
Normal file
70
test/pleroma/http/tls_test.exs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.TLSTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Pleroma.HTTP.TLS
|
||||
|
||||
describe "ssl_options/1" do
|
||||
test "verifies peers against the CAStore bundle" do
|
||||
opts = TLS.ssl_options()
|
||||
|
||||
assert opts[:verify] == :verify_peer
|
||||
assert opts[:cacertfile] == CAStore.file_path()
|
||||
assert [match_fun: match_fun] = opts[:customize_hostname_check]
|
||||
assert is_function(match_fun, 2)
|
||||
end
|
||||
|
||||
test "does not accept certificates from the peer chain as trust anchors" do
|
||||
partial_chain = TLS.ssl_options()[:partial_chain]
|
||||
|
||||
assert partial_chain.([:certificate]) == :unknown_ca
|
||||
end
|
||||
|
||||
test "keeps CA certificates that the admin set" do
|
||||
opts = TLS.ssl_options(cacerts: [:private_ca])
|
||||
|
||||
assert opts[:cacerts] == [:private_ca]
|
||||
refute Keyword.has_key?(opts, :cacertfile)
|
||||
end
|
||||
|
||||
test "keeps the CA bundle when other options are given" do
|
||||
opts = TLS.ssl_options(versions: [:"tlsv1.2"])
|
||||
|
||||
assert opts[:versions] == [:"tlsv1.2"]
|
||||
assert opts[:cacertfile] == CAStore.file_path()
|
||||
assert opts[:verify] == :verify_peer
|
||||
end
|
||||
end
|
||||
|
||||
describe "put_hackney_ssl_options/1" do
|
||||
test "adds the TLS options" do
|
||||
opts = TLS.put_hackney_ssl_options(pool: :media)
|
||||
|
||||
assert opts[:pool] == :media
|
||||
assert opts[:ssl_options] == TLS.ssl_options()
|
||||
assert opts[:proxy_ssl_options] == TLS.ssl_options()
|
||||
end
|
||||
|
||||
test "merges given ssl_options into the TLS options" do
|
||||
opts = TLS.put_hackney_ssl_options(ssl_options: [versions: [:"tlsv1.2"]])
|
||||
|
||||
assert opts[:ssl_options] == TLS.ssl_options(versions: [:"tlsv1.2"])
|
||||
end
|
||||
|
||||
test "turns insecure into an explicit verify_none" do
|
||||
opts = TLS.put_hackney_ssl_options(insecure: true)
|
||||
|
||||
assert opts[:ssl_options] == [verify: :verify_none]
|
||||
refute Keyword.has_key?(opts, :proxy_ssl_options)
|
||||
end
|
||||
|
||||
test "secures the connection to an HTTPS proxy" do
|
||||
opts = TLS.put_hackney_ssl_options(proxy_ssl_options: [versions: [:"tlsv1.3"]])
|
||||
|
||||
assert opts[:proxy_ssl_options] == TLS.ssl_options(versions: [:"tlsv1.3"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -29,6 +29,10 @@ defmodule Pleroma.HTTPTest do
|
|||
%{method: :post, url: "http://example.com/world"} ->
|
||||
%Tesla.Env{status: 200, body: "world"}
|
||||
|
||||
# Hackney 4.9 hands back the bytes it read before the peer closed.
|
||||
%{method: :get, url: "http://example.com/truncated"} ->
|
||||
{:error, {:closed, "partial body"}}
|
||||
|
||||
%{method: :get, url: "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz"} ->
|
||||
%Tesla.Env{status: 200, body: "emoji data"}
|
||||
|
||||
|
|
@ -81,6 +85,12 @@ defmodule Pleroma.HTTPTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "get/1 with a truncated body" do
|
||||
test "drops the partial body from the error" do
|
||||
assert HTTP.get("http://example.com/truncated") == {:error, :closed}
|
||||
end
|
||||
end
|
||||
|
||||
describe "get/2 (with headers)" do
|
||||
test "returns successfully result for json content-type" do
|
||||
assert HTTP.get("http://example.com/hello", [{"content-type", "application/json"}]) ==
|
||||
|
|
|
|||
|
|
@ -7,8 +7,11 @@ defmodule Pleroma.NotificationTest do
|
|||
|
||||
import Pleroma.Factory
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.FollowingRelationship
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Tests.ObanHelpers
|
||||
alias Pleroma.User
|
||||
|
|
@ -276,6 +279,22 @@ defmodule Pleroma.NotificationTest do
|
|||
assert {:ok, []} == Notification.create_notifications(status)
|
||||
end
|
||||
|
||||
test "it creates notifications for objects without content when the user has filters" do
|
||||
author = insert(:user)
|
||||
user = insert(:user)
|
||||
insert(:filter, user: user, phrase: "cofe", hide: true)
|
||||
|
||||
{:ok, status} = CommonAPI.post(author, %{status: "hey @#{user.nickname}"})
|
||||
object = Object.normalize(status, fetch: false)
|
||||
|
||||
{:ok, _} =
|
||||
object
|
||||
|> Object.change(%{data: Map.delete(object.data, "content")})
|
||||
|> Object.update_and_set_cache()
|
||||
|
||||
assert {:ok, [_]} = Notification.create_notifications(Pleroma.Activity.get_by_id(status.id))
|
||||
end
|
||||
|
||||
test "it creates notifications if content matches with a not irreversible filter" do
|
||||
user = insert(:user)
|
||||
subscriber = insert(:user)
|
||||
|
|
@ -611,6 +630,102 @@ defmodule Pleroma.NotificationTest do
|
|||
assert other_user in enabled_receivers
|
||||
end
|
||||
|
||||
defp incoming_note_tagging(author, to, tagged_ap_ids) do
|
||||
tags = Enum.map(tagged_ap_ids, &%{"type" => "Mention", "href" => &1, "name" => &1})
|
||||
|
||||
{:ok, activity} =
|
||||
Transmogrifier.handle_incoming(%{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"type" => "Create",
|
||||
"to" => to,
|
||||
"actor" => author.ap_id,
|
||||
"object" => %{
|
||||
"type" => "Note",
|
||||
"id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(),
|
||||
"to" => to,
|
||||
"content" => "tagged",
|
||||
"tag" => tags,
|
||||
"attributedTo" => author.ap_id
|
||||
}
|
||||
})
|
||||
|
||||
activity
|
||||
end
|
||||
|
||||
defp notified_ap_ids(activity),
|
||||
do: activity |> Notification.get_notified_from_activity() |> Enum.map(& &1.ap_id)
|
||||
|
||||
test "it does not notify tagged users who cannot see a followers-only post" do
|
||||
author = insert(:user)
|
||||
tagged = insert(:user)
|
||||
|
||||
activity = incoming_note_tagging(author, [author.follower_address], [tagged.ap_id])
|
||||
|
||||
refute tagged.ap_id in notified_ap_ids(activity)
|
||||
end
|
||||
|
||||
test "it notifies tagged followers of a followers-only post" do
|
||||
author = insert(:user)
|
||||
tagged = insert(:user)
|
||||
{:ok, _, _} = Pleroma.User.follow(tagged, author)
|
||||
|
||||
activity = incoming_note_tagging(author, [author.follower_address], [tagged.ap_id])
|
||||
|
||||
assert tagged.ap_id in notified_ap_ids(activity)
|
||||
end
|
||||
|
||||
test "it does not notify tagged users who are not addressed by a direct message" do
|
||||
author = insert(:user)
|
||||
addressed = insert(:user)
|
||||
tagged = insert(:user)
|
||||
|
||||
activity = incoming_note_tagging(author, [addressed.ap_id], [tagged.ap_id])
|
||||
receivers = notified_ap_ids(activity)
|
||||
|
||||
assert addressed.ap_id in receivers
|
||||
refute tagged.ap_id in receivers
|
||||
end
|
||||
|
||||
test "it notifies tagged users of a public post even if they are not addressed" do
|
||||
author = insert(:user)
|
||||
tagged = insert(:user)
|
||||
|
||||
activity =
|
||||
incoming_note_tagging(author, [Pleroma.Constants.as_public()], [tagged.ap_id])
|
||||
|
||||
assert tagged.ap_id in notified_ap_ids(activity)
|
||||
end
|
||||
|
||||
test "it does not notify tagged users with a pending follow request" do
|
||||
author = insert(:user, is_locked: true)
|
||||
tagged = insert(:user)
|
||||
{:ok, _, _, _} = CommonAPI.follow(author, tagged)
|
||||
|
||||
activity = incoming_note_tagging(author, [author.follower_address], [tagged.ap_id])
|
||||
|
||||
refute tagged.ap_id in notified_ap_ids(activity)
|
||||
end
|
||||
|
||||
test "it ignores tags of unknown users in a followers-only post" do
|
||||
author = insert(:user)
|
||||
tagged = insert(:user)
|
||||
|
||||
follower = insert(:user)
|
||||
{:ok, _, _} = Pleroma.User.follow(follower, author)
|
||||
|
||||
activity =
|
||||
incoming_note_tagging(author, [author.follower_address], [
|
||||
"https://unknown.example/users/nobody",
|
||||
tagged.ap_id,
|
||||
follower.ap_id
|
||||
])
|
||||
|
||||
receivers = notified_ap_ids(activity)
|
||||
|
||||
refute tagged.ap_id in receivers
|
||||
assert follower.ap_id in receivers
|
||||
end
|
||||
|
||||
test "it does not send notifications to users who are only cc in new messages" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
|
|
|||
|
|
@ -127,4 +127,56 @@ defmodule Pleroma.Object.ContainmentTest do
|
|||
:ok = Containment.contain_child(data)
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_actor/1" do
|
||||
test "returns nil for actor shapes without an id" do
|
||||
for actor <- [%{"type" => "Person"}, %{"id" => 1}, 1, true] do
|
||||
assert nil == Containment.get_actor(%{"actor" => actor})
|
||||
assert nil == Containment.get_actor(%{"actor" => nil, "attributedTo" => actor})
|
||||
end
|
||||
|
||||
assert nil == Containment.get_actor(%{})
|
||||
end
|
||||
|
||||
test "contain_origin/2 rejects actor shapes without an id" do
|
||||
assert :error ==
|
||||
Containment.contain_origin("https://a.test/o/1", %{
|
||||
"attributedTo" => %{"type" => "Person"}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
describe "contain_id_to_fetch/2" do
|
||||
test "matches the exact id" do
|
||||
assert :ok =
|
||||
Containment.contain_id_to_fetch("https://a.test/o/1", %{
|
||||
"id" => "https://a.test/o/1"
|
||||
})
|
||||
end
|
||||
|
||||
test "ignores fragments, a trailing slash and the host case" do
|
||||
assert :ok =
|
||||
Containment.contain_id_to_fetch("https://A.test/o/1/", %{
|
||||
"id" => "https://a.test/o/1#x"
|
||||
})
|
||||
end
|
||||
|
||||
test "rejects another id on the same host" do
|
||||
assert :error =
|
||||
Containment.contain_id_to_fetch("https://a.test/o/1", %{
|
||||
"id" => "https://a.test/o/2"
|
||||
})
|
||||
end
|
||||
|
||||
test "rejects a different query" do
|
||||
assert :error =
|
||||
Containment.contain_id_to_fetch("https://a.test/o?page=1", %{
|
||||
"id" => "https://a.test/o?page=2"
|
||||
})
|
||||
end
|
||||
|
||||
test "rejects documents without an id" do
|
||||
assert :error = Containment.contain_id_to_fetch("https://a.test/o/1", %{})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ defmodule Pleroma.Object.FetcherTest do
|
|||
%{method: :get, url: "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
# Pleroma redirects /notice/:id to the object id
|
||||
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()
|
||||
}
|
||||
|
|
@ -92,6 +94,27 @@ defmodule Pleroma.Object.FetcherTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "rate limiting" do
|
||||
test "a backed-off host gets no further requests" do
|
||||
test_pid = self()
|
||||
|
||||
mock(fn %{method: :get, url: url} ->
|
||||
send(test_pid, {:request, url})
|
||||
%Tesla.Env{status: 429, headers: [{"retry-after", "600"}]}
|
||||
end)
|
||||
|
||||
first = "https://ratelimited.example.com/objects/1"
|
||||
second = "https://ratelimited.example.com/objects/2"
|
||||
|
||||
assert {:fetch, {:error, :ratelimit}} = Fetcher.fetch_object_from_id(first)
|
||||
assert_received {:request, ^first}
|
||||
|
||||
assert {:error, :ratelimit} = Fetcher.fetch_and_contain_remote_object_from_id(second)
|
||||
assert {:fetch, {:error, :ratelimit}} = Fetcher.fetch_object_from_id(second)
|
||||
refute_received {:request, _}
|
||||
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])
|
||||
|
|
@ -463,6 +486,78 @@ defmodule Pleroma.Object.FetcherTest do
|
|||
%{object1: local_object1, object2: local_object2}
|
||||
end
|
||||
|
||||
test "it rejects a refetched object whose actor changed", %{object1: object1} do
|
||||
insert(:user, ap_id: "https://mastodon.social/users/someone_else")
|
||||
{:ok, o} = Object.create(object1)
|
||||
|
||||
mock(fn %{method: :get, url: "https://mastodon.social/1"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
headers: [{"content-type", "application/activity+json"}],
|
||||
body:
|
||||
object1
|
||||
|> Map.merge(%{
|
||||
"actor" => "https://mastodon.social/users/someone_else",
|
||||
"attributedTo" => "https://mastodon.social/users/someone_else",
|
||||
"content" => "hijacked"
|
||||
})
|
||||
|> Jason.encode!()
|
||||
}
|
||||
end)
|
||||
|
||||
assert {:error, _} = Fetcher.refetch_object(o)
|
||||
assert Object.get_by_ap_id("https://mastodon.social/1").data["content"] == "test 1"
|
||||
end
|
||||
|
||||
test "it rejects a refetched object whose author has no id", %{object1: object1} do
|
||||
{:ok, o} = Object.create(object1)
|
||||
|
||||
mock(fn %{method: :get, url: "https://mastodon.social/1"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
headers: [{"content-type", "application/activity+json"}],
|
||||
body:
|
||||
object1
|
||||
|> Map.delete("actor")
|
||||
|> Map.put("attributedTo", %{"type" => "Person"})
|
||||
|> Jason.encode!()
|
||||
}
|
||||
end)
|
||||
|
||||
assert {:error, _} = Fetcher.refetch_object(o)
|
||||
end
|
||||
|
||||
test "it rejects a refetched object whose id changed", %{object1: object1} do
|
||||
{:ok, o} = Object.create(object1)
|
||||
|
||||
mock(fn %{method: :get, url: "https://mastodon.social/1"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
url: "https://mastodon.social/2",
|
||||
headers: [{"content-type", "application/activity+json"}],
|
||||
body: object1 |> Map.put("id", "https://mastodon.social/2") |> Jason.encode!()
|
||||
}
|
||||
end)
|
||||
|
||||
assert {:error, _} = Fetcher.refetch_object(o)
|
||||
end
|
||||
|
||||
test "it rejects a refetched object whose id differs only by a trailing slash", %{
|
||||
object1: object1
|
||||
} do
|
||||
{:ok, o} = Object.create(object1)
|
||||
|
||||
mock(fn %{method: :get, url: "https://mastodon.social/1"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
headers: [{"content-type", "application/activity+json"}],
|
||||
body: object1 |> Map.put("id", "https://mastodon.social/1/") |> Jason.encode!()
|
||||
}
|
||||
end)
|
||||
|
||||
assert {:error, _} = Fetcher.refetch_object(o)
|
||||
end
|
||||
|
||||
test "it keeps formerRepresentations if remote does not have this attr", %{object1: object1} do
|
||||
full_object1 =
|
||||
object1
|
||||
|
|
@ -642,7 +737,23 @@ defmodule Pleroma.Object.FetcherTest do
|
|||
})
|
||||
}
|
||||
|
||||
# No redirect - same domain
|
||||
# Same-domain redirect to the object id
|
||||
%{method: :get, url: "https://original.test/@actor/redirect"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
url: "https://original.test/objects/redirect-target",
|
||||
headers: [{"content-type", "application/activity+json"}],
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
"id" => "https://original.test/objects/redirect-target",
|
||||
"type" => "Note",
|
||||
"content" => "This has a same-domain redirect",
|
||||
"actor" => "https://original.test/users/actor",
|
||||
"attributedTo" => "https://original.test/users/actor"
|
||||
})
|
||||
}
|
||||
|
||||
# Same-domain redirect to a document claiming the original URL as id
|
||||
%{method: :get, url: "https://original.test/objects/same-domain-redirect"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
|
|
@ -695,12 +806,19 @@ defmodule Pleroma.Object.FetcherTest do
|
|||
test "it accepts objects with same-domain redirects" do
|
||||
assert {:ok, data} =
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(
|
||||
"https://original.test/objects/same-domain-redirect"
|
||||
"https://original.test/@actor/redirect"
|
||||
)
|
||||
|
||||
assert data["content"] == "This has a same-domain redirect"
|
||||
end
|
||||
|
||||
test "it rejects redirect targets that claim the original URL as id" do
|
||||
assert {:error, _} =
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(
|
||||
"https://original.test/objects/same-domain-redirect"
|
||||
)
|
||||
end
|
||||
|
||||
test "it handles responses without URL field (common in tests)" do
|
||||
assert {:ok, data} =
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(
|
||||
|
|
@ -711,6 +829,150 @@ defmodule Pleroma.Object.FetcherTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "id containment" do
|
||||
setup do
|
||||
note = fn id, content ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
headers: [{"content-type", "application/activity+json"}],
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
"id" => id,
|
||||
"type" => "Note",
|
||||
"content" => content,
|
||||
"actor" => "https://host.test/users/actor",
|
||||
"attributedTo" => "https://host.test/users/actor"
|
||||
})
|
||||
}
|
||||
end
|
||||
|
||||
mock(fn
|
||||
# display URL serving the canonical object
|
||||
%{method: :get, url: "https://host.test/@actor/1"} ->
|
||||
note.("https://host.test/objects/1", "display")
|
||||
|
||||
%{method: :get, url: "https://host.test/objects/1"} ->
|
||||
note.("https://host.test/objects/1", "canonical")
|
||||
|
||||
# uploaded file claiming to be another object on the same host
|
||||
%{method: :get, url: "https://host.test/media/evil.json"} ->
|
||||
note.("https://host.test/objects/2", "spoofed")
|
||||
|
||||
%{method: :get, url: "https://host.test/objects/2"} ->
|
||||
note.("https://host.test/objects/2", "real")
|
||||
|
||||
# canonical id does not serve itself
|
||||
%{method: :get, url: "https://host.test/media/loop.json"} ->
|
||||
note.("https://host.test/objects/3", "loop")
|
||||
|
||||
%{method: :get, url: "https://host.test/objects/3"} ->
|
||||
note.("https://host.test/objects/4", "other")
|
||||
|
||||
# canonical id redirects
|
||||
%{method: :get, url: "https://host.test/media/redirect.json"} ->
|
||||
note.("https://host.test/objects/5", "redirect")
|
||||
|
||||
%{method: :get, url: "https://host.test/objects/5"} ->
|
||||
%{note.("https://host.test/objects/5", "moved") | url: "https://host.test/media/x"}
|
||||
|
||||
%{method: :get, url: "https://host.test/objects/slash"} ->
|
||||
note.("https://host.test/objects/slash/", "slash")
|
||||
|
||||
%{method: :get, url: "https://host.test/objects/no-id"} ->
|
||||
note.(nil, "no id")
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "it refetches display URLs from the canonical id" do
|
||||
assert {:ok, %{"id" => "https://host.test/objects/1", "content" => "canonical"}} =
|
||||
Fetcher.fetch_and_contain_remote_object_from_id("https://host.test/@actor/1")
|
||||
end
|
||||
|
||||
test "it does not trust a document claiming another id on the same host" do
|
||||
assert {:ok, %{"id" => "https://host.test/objects/2", "content" => "real"}} =
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(
|
||||
"https://host.test/media/evil.json"
|
||||
)
|
||||
end
|
||||
|
||||
test "it refetches at most once" do
|
||||
assert {:error, :id_mismatch} =
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(
|
||||
"https://host.test/media/loop.json"
|
||||
)
|
||||
end
|
||||
|
||||
test "it does not follow redirects when refetching from the canonical id" do
|
||||
assert {:error, _} =
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(
|
||||
"https://host.test/media/redirect.json"
|
||||
)
|
||||
end
|
||||
|
||||
test "it ignores a trailing slash" do
|
||||
assert {:ok, %{"content" => "slash"}} =
|
||||
Fetcher.fetch_and_contain_remote_object_from_id("https://host.test/objects/slash")
|
||||
end
|
||||
|
||||
test "it accepts ids that only differ by URL encoding" do
|
||||
clear_config(:test_url_encoding, true)
|
||||
|
||||
mock(fn %{method: :get, url: url} ->
|
||||
# the encoder sends uppercase escapes
|
||||
assert url in [
|
||||
"https://host.test/users/%E3%81%82",
|
||||
"https://host.test/notes/%E3%81%82"
|
||||
]
|
||||
|
||||
id =
|
||||
if url =~ "users",
|
||||
do: "https://host.test/users/あ",
|
||||
else: "https://host.test/notes/%e3%81%82"
|
||||
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
url: url,
|
||||
headers: [{"content-type", "application/activity+json"}],
|
||||
body: Jason.encode!(%{"id" => id, "type" => "Note"})
|
||||
}
|
||||
end)
|
||||
|
||||
assert {:ok, _} =
|
||||
Fetcher.fetch_and_contain_remote_object_from_id("https://host.test/users/あ")
|
||||
|
||||
assert {:ok, _} =
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(
|
||||
"https://host.test/notes/%e3%81%82"
|
||||
)
|
||||
end
|
||||
|
||||
test "it rejects objects whose author has no id" do
|
||||
mock(fn %{method: :get, url: "https://host.test/objects/anon" = url} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
headers: [{"content-type", "application/activity+json"}],
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
"id" => url,
|
||||
"type" => "Note",
|
||||
"content" => "anon",
|
||||
"attributedTo" => %{"type" => "Person", "name" => "nobody"}
|
||||
})
|
||||
}
|
||||
end)
|
||||
|
||||
assert {:containment, :error} =
|
||||
Fetcher.fetch_object_from_id("https://host.test/objects/anon")
|
||||
end
|
||||
|
||||
test "it rejects documents without an id" do
|
||||
assert {:error, _} =
|
||||
Fetcher.fetch_and_contain_remote_object_from_id("https://host.test/objects/no-id")
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch with history" do
|
||||
setup do
|
||||
object2 = %{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Repo.Migrations.BackfillInstancesFromUsersTest do
|
||||
use Pleroma.DataCase
|
||||
import Pleroma.Factory
|
||||
import Pleroma.Tests.Helpers
|
||||
|
||||
alias Pleroma.Instances.Instance
|
||||
|
||||
setup_all do: require_migration("20261028000000_backfill_instances_from_users")
|
||||
|
||||
test "up/0 adds the hosts of remote users and keeps existing records", %{migration: migration} do
|
||||
unreachable_since = ~N[2020-01-01 00:00:00.000000]
|
||||
insert(:instance, host: "known.example", unreachable_since: unreachable_since)
|
||||
|
||||
insert(:user, local: false, ap_id: "https://known.example/users/a")
|
||||
insert(:user, local: false, ap_id: "https://new.example/users/a")
|
||||
insert(:user, local: false, ap_id: "https://new.example/users/b")
|
||||
insert(:user, local: false, ap_id: "https://ported.example:8443/users/c")
|
||||
insert(:user, local: false, ap_id: "https://user@userinfo.example/users/d")
|
||||
insert(:user, local: false, ap_id: "https://[2001:db8::1]/users/e")
|
||||
insert(:user, local: true)
|
||||
|
||||
migration.up()
|
||||
|
||||
hosts = Instance |> Repo.all() |> Enum.map(& &1.host) |> Enum.sort()
|
||||
assert hosts == ["known.example", "new.example", "ported.example", "userinfo.example"]
|
||||
|
||||
assert %{unreachable_since: ^unreachable_since} = Repo.get_by(Instance, host: "known.example")
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Repo.Migrations.UploadFilterExiftoolToExifTest do
|
||||
use Pleroma.DataCase
|
||||
import Pleroma.Factory
|
||||
import Pleroma.Tests.Helpers
|
||||
alias Pleroma.ConfigDB
|
||||
|
||||
setup_all do: require_migration("20261027000000_upload_filter_exiftool_to_exif")
|
||||
|
||||
defp filters do
|
||||
ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Upload}).value[:filters]
|
||||
end
|
||||
|
||||
test "up/0 renames the Exiftool filters", %{migration: migration} do
|
||||
insert(:config,
|
||||
group: :pleroma,
|
||||
key: Pleroma.Upload,
|
||||
value: [
|
||||
filters: [
|
||||
Pleroma.Upload.Filter.Dedupe,
|
||||
Pleroma.Upload.Filter.Exiftool,
|
||||
Pleroma.Upload.Filter.Exiftool.StripLocation,
|
||||
Pleroma.Upload.Filter.Exiftool.ReadDescription
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
migration.up()
|
||||
|
||||
assert filters() == [
|
||||
Pleroma.Upload.Filter.Dedupe,
|
||||
Pleroma.Upload.Filter.Exif.StripLocation,
|
||||
Pleroma.Upload.Filter.Exif.ReadDescription
|
||||
]
|
||||
end
|
||||
|
||||
test "down/0 restores the Exiftool filters", %{migration: migration} do
|
||||
insert(:config,
|
||||
group: :pleroma,
|
||||
key: Pleroma.Upload,
|
||||
value: [
|
||||
filters: [
|
||||
Pleroma.Upload.Filter.Exif.StripLocation,
|
||||
Pleroma.Upload.Filter.Exif.ReadDescription
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
migration.down()
|
||||
|
||||
assert filters() == [
|
||||
Pleroma.Upload.Filter.Exiftool.StripLocation,
|
||||
Pleroma.Upload.Filter.Exiftool.ReadDescription
|
||||
]
|
||||
end
|
||||
|
||||
test "up/0 does nothing without a database config", %{migration: migration} do
|
||||
assert migration.up() == nil
|
||||
assert ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Upload}) == nil
|
||||
end
|
||||
|
||||
test "up/0 does not add filters to a database config without them", %{migration: migration} do
|
||||
insert(:config, group: :pleroma, key: Pleroma.Upload, value: [uploader: Pleroma.Uploaders.S3])
|
||||
|
||||
migration.up()
|
||||
|
||||
value = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Upload}).value
|
||||
assert value == [uploader: Pleroma.Uploaders.S3]
|
||||
end
|
||||
end
|
||||
|
|
@ -27,6 +27,21 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
|
||||
setup :verify_on_exit!
|
||||
|
||||
# Every path that obtains a client ref releases it, so tests that do not assert
|
||||
# on the release still need close/1 to be callable. Recording each call lets
|
||||
# the tests below pin *exactly* once: a Mox `expect` would silently fall
|
||||
# through to this stub on a second call.
|
||||
setup do
|
||||
test_pid = self()
|
||||
|
||||
stub(ClientMock, :close, fn client ->
|
||||
send(test_pid, {:closed, client})
|
||||
:ok
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp start_bandit(url, opts \\ []) do
|
||||
start_supervised!(
|
||||
{Bandit,
|
||||
|
|
@ -183,6 +198,100 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
assert conn.status == 200
|
||||
assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, nil}
|
||||
end
|
||||
|
||||
test "uses max_read_duration as the default HTTP receive timeout", %{conn: conn} do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/timeout", _, _, opts ->
|
||||
assert opts[:recv_timeout] == 123
|
||||
{:error, :timeout}
|
||||
end)
|
||||
|
||||
capture_log(fn -> ReverseProxy.call(conn, "/timeout", max_read_duration: 123) end)
|
||||
end
|
||||
|
||||
test "preserves an explicit HTTP receive timeout", %{conn: conn} do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/timeout", _, _, opts ->
|
||||
assert opts[:recv_timeout] == 456
|
||||
{:error, :timeout}
|
||||
end)
|
||||
|
||||
capture_log(fn ->
|
||||
ReverseProxy.call(conn, "/timeout", max_read_duration: 123, http: [recv_timeout: 456])
|
||||
end)
|
||||
end
|
||||
|
||||
test "keeps unlimited read durations unlimited", %{conn: conn} do
|
||||
for {max_read_duration, url} <- [{0, "/zero-timeout"}, {:infinity, "/infinite-timeout"}] do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, ^url, _, _, opts ->
|
||||
assert opts[:recv_timeout] == :infinity
|
||||
{:error, :timeout}
|
||||
end)
|
||||
|
||||
capture_log(fn ->
|
||||
ReverseProxy.call(conn, url, max_read_duration: max_read_duration)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# ReverseProxy releases exactly what it abandons. A body it streamed to the end
|
||||
# is the client's to release, mirroring how the gun client frees its stream on
|
||||
# :done -- see Pleroma.ReverseProxy.Client.Hackney.stream_body/1 for why an
|
||||
# abandoned Hackney ref never gets reaped on its own.
|
||||
describe "client ref release" do
|
||||
test "leaves a fully streamed body for the client to release", %{conn: conn} do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/release", _, _, _ ->
|
||||
{:ok, 200, [{"content-type", "image/png"}], %{url: "/release"}}
|
||||
end)
|
||||
|> expect(:stream_body, fn _ -> :done end)
|
||||
|
||||
conn = ReverseProxy.call(conn, "/release")
|
||||
|
||||
assert conn.halted
|
||||
refute_received {:closed, _}
|
||||
end
|
||||
|
||||
test "releases the client when content-length exceeds max_body_length", %{conn: conn} do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/release-huge", _, _, _ ->
|
||||
{:ok, 200, [{"content-length", "102227307"}], %{url: "/release-huge"}}
|
||||
end)
|
||||
|
||||
assert capture_log(fn ->
|
||||
ReverseProxy.call(conn, "/release-huge", max_body_length: 25 * 1_048_576)
|
||||
end) =~ ":body_too_large"
|
||||
|
||||
assert_received {:closed, %{url: "/release-huge"}}
|
||||
refute_received {:closed, _}
|
||||
end
|
||||
|
||||
test "releases the client when the upstream status is not proxiable", %{conn: conn} do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/release-418", _, _, _ ->
|
||||
{:ok, 418, [], %{url: "/release-418"}}
|
||||
end)
|
||||
|
||||
assert capture_log(fn -> ReverseProxy.call(conn, "/release-418") end) =~
|
||||
"failed with HTTP status 418"
|
||||
|
||||
assert_received {:closed, %{url: "/release-418"}}
|
||||
refute_received {:closed, _}
|
||||
end
|
||||
|
||||
test "has nothing to release when the reply carries no ref", %{conn: conn} do
|
||||
ClientMock
|
||||
|> expect(:request, fn :head, "/release-headless", _, _, _ ->
|
||||
{:ok, 200, [{"content-type", "image/png"}]}
|
||||
end)
|
||||
|
||||
conn = ReverseProxy.call(Map.put(conn, :method, "HEAD"), "/release-headless")
|
||||
|
||||
assert conn.status == 200
|
||||
refute_received {:closed, _}
|
||||
end
|
||||
end
|
||||
|
||||
test "use Pleroma's user agent in the request; don't pass the client's", %{conn: conn} do
|
||||
|
|
@ -398,7 +507,7 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
refute :timeout in responses
|
||||
end
|
||||
|
||||
defp stream_mock(invokes, with_close? \\ false) do
|
||||
defp stream_mock(invokes) do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/stream-bytes/" <> length, _, _, _ ->
|
||||
Registry.register(ClientMock, "/stream-bytes/" <> length, 0)
|
||||
|
|
@ -424,10 +533,6 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
:done
|
||||
end
|
||||
end)
|
||||
|
||||
if with_close? do
|
||||
expect(ClientMock, :close, fn _ -> :ok end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "max_body" do
|
||||
|
|
@ -458,7 +563,7 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
end
|
||||
|
||||
test "max_body_length returns error if streaming body more than that option", %{conn: conn} do
|
||||
stream_mock(3, true)
|
||||
stream_mock(3)
|
||||
|
||||
log =
|
||||
capture_log(fn ->
|
||||
|
|
@ -469,6 +574,9 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
|
||||
assert log =~
|
||||
"Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large"
|
||||
|
||||
assert_received {:closed, %{url: "/stream-bytes/50"}}
|
||||
refute_received {:closed, _}
|
||||
end
|
||||
|
||||
test "max_body_length accepts a body exactly at the limit", %{conn: conn} do
|
||||
|
|
|
|||
|
|
@ -44,4 +44,31 @@ defmodule Pleroma.Search.DatabaseSearchTest do
|
|||
|
||||
assert result.id == other_post.id
|
||||
end
|
||||
|
||||
describe "gin_fuzzy_search_limit" do
|
||||
defp fuzzy_limit do
|
||||
%{rows: [[value]]} = Pleroma.Repo.query!("SHOW gin_fuzzy_search_limit")
|
||||
value
|
||||
end
|
||||
|
||||
test "is not set on normal database connections" do
|
||||
assert fuzzy_limit() == "0"
|
||||
end
|
||||
|
||||
test "is only set for the search query when configured" do
|
||||
clear_config([Search, :gin_fuzzy_search_limit], nil)
|
||||
assert Search.with_fuzzy_limit(&fuzzy_limit/0) == "0"
|
||||
|
||||
clear_config([Search, :gin_fuzzy_search_limit], 10_000)
|
||||
assert Search.with_fuzzy_limit(&fuzzy_limit/0) == "10000"
|
||||
end
|
||||
|
||||
test "search still works with a limit" do
|
||||
clear_config([Search, :gin_fuzzy_search_limit], 10_000)
|
||||
{:ok, post} = CommonAPI.post(insert(:user), %{status: "it's wednesday my dudes"})
|
||||
|
||||
assert [%{id: id}] = Search.search(nil, "wednesday")
|
||||
assert id == post.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -116,6 +116,135 @@ defmodule Pleroma.SignatureTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "check_signature_policy/2" do
|
||||
@now ~U[2026-10-27 12:00:00Z]
|
||||
|
||||
defp policy_conn(method, signed_headers, extra_headers, extra_params \\ "") do
|
||||
signature =
|
||||
~s(keyId="https://remote.example/users/a#main-key",headers="#{signed_headers}") <>
|
||||
extra_params <> ~s(,signature="fake")
|
||||
|
||||
%Plug.Conn{
|
||||
method: method,
|
||||
req_headers: [{"signature", signature}, {"host", "pleroma.example"} | extra_headers],
|
||||
assigns: %{digest: "SHA-256=abc"}
|
||||
}
|
||||
end
|
||||
|
||||
defp date_header(seconds_ago) do
|
||||
date = DateTime.add(@now, -seconds_ago) |> DateTime.to_naive()
|
||||
{"date", Signature.signed_date(date)}
|
||||
end
|
||||
|
||||
test "accepts a fresh signed GET (Pleroma, Mastodon, Misskey style)" do
|
||||
for headers <- ["(request-target) host date", "(request-target) host date accept"] do
|
||||
conn = policy_conn("GET", headers, [date_header(10)])
|
||||
assert Signature.check_signature_policy(conn, @now) == :ok
|
||||
end
|
||||
end
|
||||
|
||||
test "accepts a fresh signed POST covering the digest" do
|
||||
conn =
|
||||
policy_conn("POST", "(request-target) host content-length digest date", [
|
||||
date_header(10),
|
||||
{"digest", "SHA-256=abc"}
|
||||
])
|
||||
|
||||
assert Signature.check_signature_policy(conn, @now) == :ok
|
||||
end
|
||||
|
||||
test "accepts (created) instead of date (GoToSocial style)" do
|
||||
created = DateTime.to_unix(@now) - 10
|
||||
|
||||
conn =
|
||||
policy_conn(
|
||||
"POST",
|
||||
"(request-target) (created) host digest",
|
||||
[{"digest", "SHA-256=abc"}],
|
||||
",algorithm=\"hs2019\",created=#{created}"
|
||||
)
|
||||
|
||||
assert Signature.check_signature_policy(conn, @now) == :ok
|
||||
end
|
||||
|
||||
test "rejects signatures that cover neither date nor (created)" do
|
||||
conn = policy_conn("GET", "(request-target) host", [date_header(10)])
|
||||
assert Signature.check_signature_policy(conn, @now) == {:error, :date_not_signed}
|
||||
end
|
||||
|
||||
test "rejects a body request whose digest is not signed" do
|
||||
conn =
|
||||
policy_conn("POST", "(request-target) host date", [
|
||||
date_header(10),
|
||||
{"digest", "SHA-256=abc"}
|
||||
])
|
||||
|
||||
assert Signature.check_signature_policy(conn, @now) == {:error, :digest_not_signed}
|
||||
end
|
||||
|
||||
test "rejects a signed date that is missing or unparsable" do
|
||||
conn = policy_conn("GET", "(request-target) host date", [])
|
||||
assert Signature.check_signature_policy(conn, @now) == {:error, :invalid_date}
|
||||
|
||||
conn = policy_conn("GET", "(request-target) host date", [{"date", "yesterday"}])
|
||||
assert Signature.check_signature_policy(conn, @now) == {:error, :invalid_date}
|
||||
end
|
||||
|
||||
test "rejects more than one date header" do
|
||||
conn =
|
||||
policy_conn("GET", "(request-target) host date", [date_header(10), date_header(9000)])
|
||||
|
||||
assert Signature.check_signature_policy(conn, @now) == {:error, :invalid_date}
|
||||
end
|
||||
|
||||
test "rejects a signed digest when the body was not hashed" do
|
||||
conn =
|
||||
policy_conn("POST", "(request-target) host date digest", [
|
||||
date_header(10),
|
||||
{"digest", "SHA-256=abc"}
|
||||
])
|
||||
|> Map.put(:assigns, %{})
|
||||
|
||||
assert Signature.check_signature_policy(conn, @now) == {:error, :body_not_hashed}
|
||||
end
|
||||
|
||||
test "rejects a signed date that is too old or too far in the future" do
|
||||
clear_config([:activitypub, :signature_max_age], 7200)
|
||||
clear_config([:activitypub, :signature_max_clock_skew], 3600)
|
||||
|
||||
conn = policy_conn("GET", "(request-target) host date", [date_header(7200)])
|
||||
assert Signature.check_signature_policy(conn, @now) == :ok
|
||||
|
||||
conn = policy_conn("GET", "(request-target) host date", [date_header(7201)])
|
||||
assert Signature.check_signature_policy(conn, @now) == {:error, :date_too_old}
|
||||
|
||||
conn = policy_conn("GET", "(request-target) host date", [date_header(-3600)])
|
||||
assert Signature.check_signature_policy(conn, @now) == :ok
|
||||
|
||||
conn = policy_conn("GET", "(request-target) host date", [date_header(-3601)])
|
||||
assert Signature.check_signature_policy(conn, @now) == {:error, :date_in_future}
|
||||
end
|
||||
|
||||
test "rejects a (created) timestamp that is too old" do
|
||||
clear_config([:activitypub, :signature_max_age], 60)
|
||||
created = DateTime.to_unix(@now) - 61
|
||||
|
||||
conn =
|
||||
policy_conn("GET", "(request-target) (created) host", [], ",created=#{created}")
|
||||
|
||||
assert Signature.check_signature_policy(conn, @now) == {:error, :date_too_old}
|
||||
end
|
||||
|
||||
test "uses the cavage default of a signed date when headers are not given" do
|
||||
conn = %Plug.Conn{
|
||||
method: "GET",
|
||||
req_headers: [{"signature", "keyId=\"https://remote.example/users/a#main-key\""}]
|
||||
}
|
||||
|
||||
assert Signature.check_signature_policy(conn, @now) == {:error, :invalid_date}
|
||||
end
|
||||
end
|
||||
|
||||
describe "key_id_to_actor_id/1" do
|
||||
test "it properly deduces the actor id for misskey" do
|
||||
assert Signature.key_id_to_actor_id("https://example.com/users/1234/publickey") ==
|
||||
|
|
|
|||
87
test/pleroma/tesla/middleware/max_body_test.exs
Normal file
87
test/pleroma/tesla/middleware/max_body_test.exs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Tesla.Middleware.MaxBodyTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Pleroma.Tesla.Middleware.MaxBody
|
||||
|
||||
defp client(adapter_opts, response) do
|
||||
Tesla.client([{MaxBody, nil}], fn env ->
|
||||
{:ok, %{env | status: 200, headers: response.headers, body: response.body}}
|
||||
end)
|
||||
|> then(fn c -> {c, [opts: [adapter: adapter_opts]]} end)
|
||||
end
|
||||
|
||||
defp get(adapter_opts, response) do
|
||||
{c, opts} = client(adapter_opts, response)
|
||||
Tesla.get(c, "http://example.com/", opts)
|
||||
end
|
||||
|
||||
test "passes a body within the limit through untouched" do
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "hello"}} =
|
||||
get([max_body: 100], %{headers: [], body: "hello"})
|
||||
end
|
||||
|
||||
test "rejects a body larger than the limit" do
|
||||
assert {:error, :body_too_large} =
|
||||
get([max_body: 4], %{headers: [], body: "hello"})
|
||||
end
|
||||
|
||||
test "rejects when content-length exceeds the limit" do
|
||||
assert {:error, :body_too_large} =
|
||||
get([max_body: 4], %{headers: [{"content-length", "1000"}], body: ""})
|
||||
end
|
||||
|
||||
test "allows a content-length equal to the limit" do
|
||||
assert {:ok, %Tesla.Env{status: 200}} =
|
||||
get([max_body: 5], %{headers: [{"content-length", "5"}], body: "hello"})
|
||||
end
|
||||
|
||||
test "ignores an unparsable content-length" do
|
||||
assert {:ok, %Tesla.Env{status: 200}} =
|
||||
get([max_body: 100], %{headers: [{"content-length", "banana"}], body: "hi"})
|
||||
end
|
||||
|
||||
test "does nothing without a max_body option" do
|
||||
big = String.duplicate("x", 10_000)
|
||||
|
||||
assert {:ok, %Tesla.Env{body: ^big}} =
|
||||
get([], %{headers: [{"content-length", "10000"}], body: big})
|
||||
end
|
||||
|
||||
# body_as: :chunks hands back a client map, not a binary; the caller streams
|
||||
# it and enforces its own limit (see Pleroma.Web.RichMedia.Helpers).
|
||||
test "leaves a non-binary streaming body alone" do
|
||||
body = %{pid: self(), stream: make_ref()}
|
||||
|
||||
assert {:ok, %Tesla.Env{body: ^body}} =
|
||||
get([max_body: 1], %{headers: [], body: body})
|
||||
end
|
||||
|
||||
# Rejecting a stream on the header alone would strand the connection lease:
|
||||
# the env carrying the pid/stream is the caller's only handle to release it.
|
||||
test "does not reject a streaming body on content-length alone" do
|
||||
body = %{pid: self(), stream: make_ref()}
|
||||
|
||||
assert {:ok, %Tesla.Env{body: ^body}} =
|
||||
get([max_body: 1], %{headers: [{"content-length", "20000000"}], body: body})
|
||||
end
|
||||
|
||||
test "ignores content-length on a HEAD response" do
|
||||
{c, opts} = client([max_body: 4], %{headers: [{"content-length", "9000000"}], body: ""})
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200}} = Tesla.head(c, "http://example.com/", opts)
|
||||
end
|
||||
|
||||
test "ignores content-length on a bodyless status" do
|
||||
client =
|
||||
Tesla.client([{MaxBody, nil}], fn env ->
|
||||
{:ok, %{env | status: 304, headers: [{"content-length", "9000000"}], body: ""}}
|
||||
end)
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 304}} =
|
||||
Tesla.get(client, "http://example.com/", opts: [adapter: [max_body: 4]])
|
||||
end
|
||||
end
|
||||
85
test/pleroma/upload/filter/exif/read_description_test.exs
Normal file
85
test/pleroma/upload/filter/exif/read_description_test.exs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Upload.Filter.Exif.ReadDescriptionTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.Upload.Filter.Exif.ReadDescription
|
||||
|
||||
defp upload(fixture, attrs \\ []) do
|
||||
path = Path.absname(Path.join("test/fixtures", fixture))
|
||||
|
||||
struct!(
|
||||
%Pleroma.Upload{
|
||||
name: fixture,
|
||||
content_type: "image/" <> String.trim_leading(Path.extname(fixture), "."),
|
||||
path: path,
|
||||
tempfile: path,
|
||||
description: nil
|
||||
},
|
||||
attrs
|
||||
)
|
||||
end
|
||||
|
||||
test "keeps description when not empty" do
|
||||
upload =
|
||||
upload("image_with_imagedescription_and_caption-abstract.jpg",
|
||||
description: "Some description"
|
||||
)
|
||||
|
||||
assert ReadDescription.filter(upload) == {:ok, :noop}
|
||||
end
|
||||
|
||||
test "otherwise returns ImageDescription when present" do
|
||||
upload = upload("image_with_imagedescription_and_caption-abstract.jpg")
|
||||
|
||||
assert ReadDescription.filter(upload) ==
|
||||
{:ok, :filtered, %{upload | description: "a descriptive white pixel"}}
|
||||
end
|
||||
|
||||
test "ignores stray data after the image" do
|
||||
upload = upload("image_with_imagedescription_and_caption-abstract_and_stray_data_after.png")
|
||||
|
||||
assert {:ok, :filtered, %{description: "a descriptive white pixel"}} =
|
||||
ReadDescription.filter(upload)
|
||||
|
||||
assert ReadDescription.filter(upload("image_with_stray_data_after.png")) == {:ok, :noop}
|
||||
end
|
||||
|
||||
test "otherwise returns iptc:Caption-Abstract when present" do
|
||||
upload = upload("image_with_caption-abstract.jpg")
|
||||
|
||||
assert ReadDescription.filter(upload) ==
|
||||
{:ok, :filtered, %{upload | description: "an abstract white pixel"}}
|
||||
end
|
||||
|
||||
test "reads ImageDescription from WebP and HEIC" do
|
||||
assert {:ok, :filtered, %{description: "a webp with a location"}} =
|
||||
ReadDescription.filter(upload("strip_location_gps.webp"))
|
||||
|
||||
assert {:ok, :filtered, %{description: "a heic with a location"}} =
|
||||
ReadDescription.filter(upload("strip_location_gps.heic"))
|
||||
end
|
||||
|
||||
test "does nothing when there is no description" do
|
||||
assert ReadDescription.filter(upload("image_with_no_description.jpg")) == {:ok, :noop}
|
||||
end
|
||||
|
||||
test "ignores descriptions that exceed the maximum length" do
|
||||
clear_config([:instance, :description_limit], 5)
|
||||
|
||||
upload = upload("image_with_imagedescription_and_caption-abstract.jpg")
|
||||
assert ReadDescription.filter(upload) == {:ok, :noop}
|
||||
end
|
||||
|
||||
test "ignores content with only whitespace" do
|
||||
upload = upload("image_with_imagedescription_and_caption-abstract_whitespaces.jpg")
|
||||
assert ReadDescription.filter(upload) == {:ok, :noop}
|
||||
end
|
||||
|
||||
test "does nothing when the file can't be read" do
|
||||
upload = %{upload("image.jpg") | tempfile: Path.absname("test/fixtures/non-existant_tmp.jpg")}
|
||||
assert ReadDescription.filter(upload) == {:ok, :noop}
|
||||
end
|
||||
end
|
||||
72
test/pleroma/upload/filter/exif/strip_location_test.exs
Normal file
72
test/pleroma/upload/filter/exif/strip_location_test.exs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Upload.Filter.Exif.StripLocationTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
import Pleroma.Tests.ExifHelpers
|
||||
|
||||
alias Pleroma.Upload.Filter.Exif.StripLocation
|
||||
|
||||
for {fixture, content_type} <- [
|
||||
{"DSCN0010.jpg", "image/jpeg"},
|
||||
{"DSCN0010.png", "image/png"},
|
||||
{"strip_location_gps.webp", "image/webp"},
|
||||
{"strip_location_gps.heic", "image/heic"},
|
||||
{"strip_location_gps_grid.heic", "image/heic"},
|
||||
{"strip_location_gps.tiff", "image/tiff"}
|
||||
] do
|
||||
test "strips GPS data from #{fixture} (#{content_type})" do
|
||||
upload = upload_copy(unquote(fixture), unquote(content_type))
|
||||
assert gps_tags(upload.tempfile) =~ "GPS Latitude"
|
||||
|
||||
assert StripLocation.filter(upload) == {:ok, :filtered}
|
||||
assert gps_tags(upload.tempfile) == ""
|
||||
end
|
||||
end
|
||||
|
||||
test "strips GPS data from XMP, including DJI drone positions" do
|
||||
for fixture <- ["xmp_gps.jpg", "dji_gps.jpg"] do
|
||||
upload = upload_copy(fixture, "image/jpeg")
|
||||
assert exiftool_xmp(upload.tempfile) =~ "GPSLatitude"
|
||||
|
||||
assert StripLocation.filter(upload) == {:ok, :filtered}
|
||||
refute exiftool_xmp(upload.tempfile) =~ ~r/GPS|AbsoluteAltitude/
|
||||
end
|
||||
end
|
||||
|
||||
defp exiftool_xmp(path) do
|
||||
{output, 0} = System.cmd("exiftool", ["-m", "-s", "-xmp:all", path])
|
||||
output
|
||||
end
|
||||
|
||||
test "keeps other EXIF data in HEIC" do
|
||||
upload = upload_copy("strip_location_gps.heic", "image/heic")
|
||||
|
||||
assert StripLocation.filter(upload) == {:ok, :filtered}
|
||||
assert tag(upload.tempfile, "ImageDescription") == "a heic with a location"
|
||||
assert tag(upload.tempfile, "ImageSize") == "64x48"
|
||||
end
|
||||
|
||||
test "does nothing when there is no GPS data" do
|
||||
upload = upload_copy("image.jpg", "image/jpeg")
|
||||
original = File.read!(upload.tempfile)
|
||||
|
||||
assert StripLocation.filter(upload) == {:ok, :noop}
|
||||
assert File.read!(upload.tempfile) == original
|
||||
end
|
||||
|
||||
test "accepts PNG files with data after IEND" do
|
||||
upload = upload_copy("image_with_stray_data_after.png", "image/png")
|
||||
|
||||
assert {:ok, _} = StripLocation.filter(upload)
|
||||
end
|
||||
|
||||
test "ignores non-image uploads" do
|
||||
upload = upload_copy("DSCN0010.jpg", "application/octet-stream")
|
||||
|
||||
assert StripLocation.filter(upload) == {:ok, :noop}
|
||||
assert gps_tags(upload.tempfile) =~ "GPS Latitude"
|
||||
end
|
||||
end
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Upload.Filter.Exiftool.ReadDescriptionTest do
|
||||
use Pleroma.DataCase
|
||||
alias Pleroma.Upload.Filter
|
||||
|
||||
@uploads %Pleroma.Upload{
|
||||
name: "image_with_imagedescription_and_caption-abstract.jpg",
|
||||
content_type: "image/jpeg",
|
||||
path: Path.absname("test/fixtures/image_with_imagedescription_and_caption-abstract.jpg"),
|
||||
tempfile: Path.absname("test/fixtures/image_with_imagedescription_and_caption-abstract.jpg"),
|
||||
description: nil
|
||||
}
|
||||
|
||||
test "keeps description when not empty" do
|
||||
uploads = %Pleroma.Upload{
|
||||
name: "image_with_imagedescription_and_caption-abstract.jpg",
|
||||
content_type: "image/jpeg",
|
||||
path: Path.absname("test/fixtures/image_with_imagedescription_and_caption-abstract.jpg"),
|
||||
tempfile:
|
||||
Path.absname("test/fixtures/image_with_imagedescription_and_caption-abstract.jpg"),
|
||||
description: "Some description"
|
||||
}
|
||||
|
||||
assert Filter.Exiftool.ReadDescription.filter(uploads) ==
|
||||
{:ok, :noop}
|
||||
end
|
||||
|
||||
test "otherwise returns ImageDescription when present" do
|
||||
uploads_after = %Pleroma.Upload{
|
||||
name: "image_with_imagedescription_and_caption-abstract.jpg",
|
||||
content_type: "image/jpeg",
|
||||
path: Path.absname("test/fixtures/image_with_imagedescription_and_caption-abstract.jpg"),
|
||||
tempfile:
|
||||
Path.absname("test/fixtures/image_with_imagedescription_and_caption-abstract.jpg"),
|
||||
description: "a descriptive white pixel"
|
||||
}
|
||||
|
||||
assert Filter.Exiftool.ReadDescription.filter(@uploads) ==
|
||||
{:ok, :filtered, uploads_after}
|
||||
end
|
||||
|
||||
test "Ignores warnings" do
|
||||
uploads = %Pleroma.Upload{
|
||||
name: "image_with_imagedescription_and_caption-abstract_and_stray_data_after.png",
|
||||
content_type: "image/png",
|
||||
path:
|
||||
Path.absname(
|
||||
"test/fixtures/image_with_imagedescription_and_caption-abstract_and_stray_data_after.png"
|
||||
),
|
||||
tempfile:
|
||||
Path.absname(
|
||||
"test/fixtures/image_with_imagedescription_and_caption-abstract_and_stray_data_after.png"
|
||||
)
|
||||
}
|
||||
|
||||
assert {:ok, :filtered, %{description: "a descriptive white pixel"}} =
|
||||
Filter.Exiftool.ReadDescription.filter(uploads)
|
||||
|
||||
uploads = %Pleroma.Upload{
|
||||
name: "image_with_stray_data_after.png",
|
||||
content_type: "image/png",
|
||||
path: Path.absname("test/fixtures/image_with_stray_data_after.png"),
|
||||
tempfile: Path.absname("test/fixtures/image_with_stray_data_after.png")
|
||||
}
|
||||
|
||||
assert {:ok, :filtered, %{description: nil}} = Filter.Exiftool.ReadDescription.filter(uploads)
|
||||
end
|
||||
|
||||
test "otherwise returns iptc:Caption-Abstract when present" do
|
||||
upload = %Pleroma.Upload{
|
||||
name: "image_with_caption-abstract.jpg",
|
||||
content_type: "image/jpeg",
|
||||
path: Path.absname("test/fixtures/image_with_caption-abstract.jpg"),
|
||||
tempfile: Path.absname("test/fixtures/image_with_caption-abstract.jpg"),
|
||||
description: nil
|
||||
}
|
||||
|
||||
upload_after = %Pleroma.Upload{
|
||||
name: "image_with_caption-abstract.jpg",
|
||||
content_type: "image/jpeg",
|
||||
path: Path.absname("test/fixtures/image_with_caption-abstract.jpg"),
|
||||
tempfile: Path.absname("test/fixtures/image_with_caption-abstract.jpg"),
|
||||
description: "an abstract white pixel"
|
||||
}
|
||||
|
||||
assert Filter.Exiftool.ReadDescription.filter(upload) ==
|
||||
{:ok, :filtered, upload_after}
|
||||
end
|
||||
|
||||
test "otherwise returns nil" do
|
||||
uploads = %Pleroma.Upload{
|
||||
name: "image_with_no_description.jpg",
|
||||
content_type: "image/jpeg",
|
||||
path: Path.absname("test/fixtures/image_with_no_description.jpg"),
|
||||
tempfile: Path.absname("test/fixtures/image_with_no_description.jpg"),
|
||||
description: nil
|
||||
}
|
||||
|
||||
assert Filter.Exiftool.ReadDescription.filter(uploads) ==
|
||||
{:ok, :filtered, uploads}
|
||||
end
|
||||
|
||||
test "Return nil when image description from EXIF data exceeds the maximum length" do
|
||||
clear_config([:instance, :description_limit], 5)
|
||||
|
||||
assert Filter.Exiftool.ReadDescription.filter(@uploads) ==
|
||||
{:ok, :filtered, @uploads}
|
||||
end
|
||||
|
||||
test "Ignores content with only whitespace" do
|
||||
uploads = %Pleroma.Upload{
|
||||
name: "non-existant.jpg",
|
||||
content_type: "image/jpeg",
|
||||
path:
|
||||
Path.absname(
|
||||
"test/fixtures/image_with_imagedescription_and_caption-abstract_whitespaces.jpg"
|
||||
),
|
||||
tempfile:
|
||||
Path.absname(
|
||||
"test/fixtures/image_with_imagedescription_and_caption-abstract_whitespaces.jpg"
|
||||
),
|
||||
description: nil
|
||||
}
|
||||
|
||||
assert Filter.Exiftool.ReadDescription.filter(uploads) ==
|
||||
{:ok, :filtered, uploads}
|
||||
end
|
||||
|
||||
test "Return nil when image description from EXIF data can't be read" do
|
||||
uploads = %Pleroma.Upload{
|
||||
name: "non-existant.jpg",
|
||||
content_type: "image/jpeg",
|
||||
path: Path.absname("test/fixtures/non-existant.jpg"),
|
||||
tempfile: Path.absname("test/fixtures/non-existant_tmp.jpg"),
|
||||
description: nil
|
||||
}
|
||||
|
||||
assert Filter.Exiftool.ReadDescription.filter(uploads) ==
|
||||
{:ok, :filtered, uploads}
|
||||
end
|
||||
end
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Upload.Filter.Exiftool.StripLocationTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
alias Pleroma.Upload.Filter
|
||||
|
||||
test "apply exiftool filter" do
|
||||
assert Pleroma.Utils.command_available?("exiftool")
|
||||
|
||||
~w{jpg png}
|
||||
|> Enum.map(fn type ->
|
||||
File.cp!(
|
||||
"test/fixtures/DSCN0010.#{type}",
|
||||
"test/fixtures/DSCN0010_tmp.#{type}"
|
||||
)
|
||||
|
||||
upload = %Pleroma.Upload{
|
||||
name: "image_with_GPS_data.#{type}",
|
||||
content_type: "image/jpeg",
|
||||
path: Path.absname("test/fixtures/DSCN0010.#{type}"),
|
||||
tempfile: Path.absname("test/fixtures/DSCN0010_tmp.#{type}")
|
||||
}
|
||||
|
||||
assert Filter.Exiftool.StripLocation.filter(upload) == {:ok, :filtered}
|
||||
|
||||
{exif_original, 0} = System.cmd("exiftool", ["-m", "test/fixtures/DSCN0010.#{type}"])
|
||||
{exif_filtered, 0} = System.cmd("exiftool", ["-m", "test/fixtures/DSCN0010_tmp.#{type}"])
|
||||
|
||||
assert String.match?(exif_original, ~r/GPS/)
|
||||
refute String.match?(exif_filtered, ~r/GPS/)
|
||||
end)
|
||||
end
|
||||
|
||||
test "verify webp, heic, svg files are skipped" do
|
||||
uploads =
|
||||
~w{webp heic svg svg+xml}
|
||||
|> Enum.map(fn type ->
|
||||
%Pleroma.Upload{
|
||||
name: "sample.#{type}",
|
||||
content_type: "image/#{type}"
|
||||
}
|
||||
end)
|
||||
|
||||
uploads
|
||||
|> Enum.each(fn upload ->
|
||||
assert Filter.Exiftool.StripLocation.filter(upload) == {:ok, :noop}
|
||||
end)
|
||||
end
|
||||
end
|
||||
31
test/pleroma/upload/filter/exiftool_test.exs
Normal file
31
test/pleroma/upload/filter/exiftool_test.exs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Upload.Filter.ExiftoolTest do
|
||||
# Instances may still configure the deprecated Exiftool filter names.
|
||||
# They must keep working, including for formats exiftool used to skip.
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
import Pleroma.Tests.ExifHelpers
|
||||
|
||||
for filter <- [Pleroma.Upload.Filter.Exiftool, Pleroma.Upload.Filter.Exiftool.StripLocation] do
|
||||
test "#{inspect(filter)} strips GPS data from HEIC uploads" do
|
||||
upload = upload_copy("strip_location_gps.heic", "image/heic")
|
||||
assert gps_tags(upload.tempfile) =~ "GPS Latitude"
|
||||
|
||||
assert Pleroma.Upload.Filter.filter([unquote(filter)], upload) == {:ok, upload}
|
||||
assert gps_tags(upload.tempfile) == ""
|
||||
end
|
||||
end
|
||||
|
||||
test "Exiftool.ReadDescription reads the description" do
|
||||
upload = upload_copy("strip_location_gps.heic", "image/heic", description: nil)
|
||||
|
||||
assert {:ok, %{description: "a heic with a location"}} =
|
||||
Pleroma.Upload.Filter.filter(
|
||||
[Pleroma.Upload.Filter.Exiftool.ReadDescription],
|
||||
upload
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -531,9 +531,9 @@ defmodule Pleroma.UserTest do
|
|||
ObanHelpers.perform_all()
|
||||
|
||||
Pleroma.Emails.UserEmail.account_confirmation_email(registered_user)
|
||||
# temporary hackney fix until hackney max_connections bug is fixed
|
||||
# https://git.pleroma.social/pleroma/pleroma/-/issues/2101
|
||||
|> Swoosh.Email.put_private(:hackney_options, ssl_options: [versions: [:"tlsv1.2"]])
|
||||
|> Swoosh.Email.put_private(:hackney_options,
|
||||
ssl_options: Pleroma.HTTP.TLS.ssl_options()
|
||||
)
|
||||
|> assert_email_sent()
|
||||
end
|
||||
|
||||
|
|
@ -1197,11 +1197,32 @@ defmodule Pleroma.UserTest do
|
|||
|
||||
res = User.get_followers(user)
|
||||
|
||||
assert length(res) == 2
|
||||
assert Enum.member?(res, follower_one)
|
||||
assert Enum.member?(res, follower_two)
|
||||
refute Enum.member?(res, not_follower)
|
||||
end
|
||||
|
||||
test "does not return deactivated followers" do
|
||||
user = insert(:user)
|
||||
follower_one = insert(:user)
|
||||
follower_two = insert(:user)
|
||||
not_follower = insert(:user)
|
||||
|
||||
{:ok, follower_one, user} = User.follow(follower_one, user)
|
||||
{:ok, follower_two, user} = User.follow(follower_two, user)
|
||||
|
||||
User.delete(follower_two)
|
||||
follower_two = User.get_cached_by_id(follower_two.id)
|
||||
|
||||
res = User.get_followers(user)
|
||||
|
||||
assert length(res) == 1
|
||||
assert Enum.member?(res, follower_one)
|
||||
refute Enum.member?(res, follower_two)
|
||||
refute Enum.member?(res, not_follower)
|
||||
end
|
||||
|
||||
test "gets all friends (followed users) for a given user" do
|
||||
user = insert(:user)
|
||||
followed_one = insert(:user)
|
||||
|
|
@ -1215,10 +1236,34 @@ defmodule Pleroma.UserTest do
|
|||
|
||||
followed_one = User.get_cached_by_ap_id(followed_one.ap_id)
|
||||
followed_two = User.get_cached_by_ap_id(followed_two.ap_id)
|
||||
|
||||
assert length(res) == 2
|
||||
assert Enum.member?(res, followed_one)
|
||||
assert Enum.member?(res, followed_two)
|
||||
refute Enum.member?(res, not_followed)
|
||||
end
|
||||
|
||||
test "does not return deactivated followed users (friends)" do
|
||||
user = insert(:user)
|
||||
followed_one = insert(:user)
|
||||
followed_two = insert(:user)
|
||||
not_followed = insert(:user)
|
||||
|
||||
{:ok, user, followed_one} = User.follow(user, followed_one)
|
||||
{:ok, user, followed_two} = User.follow(user, followed_two)
|
||||
|
||||
User.delete(followed_two)
|
||||
|
||||
res = User.get_friends(user)
|
||||
|
||||
followed_one = User.get_cached_by_id(followed_one.id)
|
||||
followed_two = User.get_cached_by_id(followed_two.id)
|
||||
|
||||
assert length(res) == 1
|
||||
assert Enum.member?(res, followed_one)
|
||||
refute Enum.member?(res, followed_two)
|
||||
refute Enum.member?(res, not_followed)
|
||||
end
|
||||
end
|
||||
|
||||
describe "updating note and follower count" do
|
||||
|
|
@ -2964,6 +3009,18 @@ defmodule Pleroma.UserTest do
|
|||
|
||||
assert [%{id: ^id2}] = User.get_familiar_followers(user3, user1)
|
||||
end
|
||||
|
||||
test "does not return deactivated users" do
|
||||
user1 = insert(:user)
|
||||
user2 = insert(:user)
|
||||
user3 = insert(:user)
|
||||
|
||||
User.follow(user1, user2)
|
||||
User.follow(user2, user3)
|
||||
{:ok, _} = User.set_activation(user2, false)
|
||||
|
||||
assert [] = User.get_familiar_followers(user3, user1)
|
||||
end
|
||||
end
|
||||
|
||||
describe "account endorsements" do
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
alias Pleroma.Activity
|
||||
alias Pleroma.Delivery
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Signature
|
||||
alias Pleroma.Tests.ObanHelpers
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
|
|
@ -49,6 +50,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
|> put_req_header("signature", "keyId=\"#{actor_id}#main-key\"")
|
||||
end
|
||||
|
||||
defp put_fake_signature(conn, actor_id, date \\ Signature.signed_date()) do
|
||||
conn
|
||||
|> put_req_header("date", date)
|
||||
|> put_req_header("digest", "SHA-256=fake-digest")
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"#{actor_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
|
||||
)
|
||||
end
|
||||
|
||||
defp expect_signature_retry_from(%User{} = signer) do
|
||||
signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured")
|
||||
|
||||
|
|
@ -792,12 +803,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
}
|
||||
}
|
||||
|
||||
Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> false end)
|
||||
Mox.stub(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> false end)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> put_req_header("date", "Fri, 29 May 2026 13:33:31 GMT")
|
||||
|> put_req_header("date", Signature.signed_date())
|
||||
|> put_req_header("content-digest", "sha-256=:fake-digest:")
|
||||
|> put_req_header(
|
||||
"signature-input",
|
||||
|
|
@ -832,12 +843,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
legacy_signature =
|
||||
"keyId=\"https://activitypubbot.example/user/ok#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
|
||||
|
||||
Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> false end)
|
||||
Mox.stub(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> false end)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> put_req_header("date", "Fri, 29 May 2026 13:33:31 GMT")
|
||||
|> put_req_header("date", Signature.signed_date())
|
||||
|> put_req_header("digest", "SHA-256=fake-digest")
|
||||
|> put_req_header("signature", "sig1=:fake-signature:")
|
||||
|> Map.update!(:req_headers, &[{"signature", legacy_signature} | &1])
|
||||
|
|
@ -871,7 +882,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> put_req_header("date", "Fri, 29 May 2026 13:33:31 GMT")
|
||||
|> put_req_header("date", Signature.signed_date())
|
||||
|> put_req_header("digest", "SHA-256=fake-digest")
|
||||
|> put_req_header(
|
||||
"signature-input",
|
||||
|
|
@ -881,12 +892,44 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
"signature",
|
||||
"keyId=\"https://activitypubbot.example/user/ok#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
|
||||
)
|
||||
|> post("/inbox", data)
|
||||
|> post("/inbox", Jason.encode!(data))
|
||||
|
||||
assert "ok" == json_response(conn, 200)
|
||||
assert [_] = all_enqueued(worker: SignatureRetryWorker)
|
||||
end
|
||||
|
||||
test "refuses a replayed signed POST instead of queueing a retry", %{conn: conn} do
|
||||
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
|
||||
stale = NaiveDateTime.add(NaiveDateTime.utc_now(), -3 * 3600)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> put_fake_signature(data["actor"], Signature.signed_date(stale))
|
||||
|> post("/inbox", Jason.encode!(data))
|
||||
|
||||
assert "error, invalid HTTP signature (date_too_old)" == json_response(conn, 401)
|
||||
assert [] == all_enqueued(worker: SignatureRetryWorker)
|
||||
assert [] == all_enqueued(worker: ReceiverWorker)
|
||||
end
|
||||
|
||||
test "refuses a signed POST that does not cover the digest", %{conn: conn} do
|
||||
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> put_req_header("date", Signature.signed_date())
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"#{data["actor"]}#main-key\",headers=\"(request-target) host date\",signature=\"fake\""
|
||||
)
|
||||
|> post("/inbox", Jason.encode!(data))
|
||||
|
||||
assert "error, invalid HTTP signature (digest_not_signed)" == json_response(conn, 401)
|
||||
assert [] == all_enqueued(worker: SignatureRetryWorker)
|
||||
end
|
||||
|
||||
test "does not create a forged post after failed signature retry", %{conn: conn} do
|
||||
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
|
||||
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
|
||||
|
|
@ -918,8 +961,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
conn
|
||||
|> assign(:valid_signature, false)
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"")
|
||||
|> post("/inbox", data)
|
||||
|> put_fake_signature(alice.ap_id)
|
||||
|> post("/inbox", Jason.encode!(data))
|
||||
|
||||
assert "ok" == json_response(conn, 200)
|
||||
|
||||
|
|
@ -950,8 +993,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
conn
|
||||
|> assign(:valid_signature, false)
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"")
|
||||
|> post("/inbox", data)
|
||||
|> put_fake_signature(alice.ap_id)
|
||||
|> post("/inbox", Jason.encode!(data))
|
||||
|
||||
assert "ok" == json_response(conn, 200)
|
||||
|
||||
|
|
@ -982,8 +1025,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
conn
|
||||
|> assign(:valid_signature, false)
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"")
|
||||
|> post("/inbox", data)
|
||||
|> put_fake_signature(alice.ap_id)
|
||||
|> post("/inbox", Jason.encode!(data))
|
||||
|
||||
assert "ok" == json_response(conn, 200)
|
||||
|
||||
|
|
@ -1025,13 +1068,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> put_req_header("date", "Thu, 25 Jul 2024 13:33:31 GMT")
|
||||
|> put_req_header("date", Signature.signed_date())
|
||||
|> put_req_header("digest", "SHA-256=fake-digest")
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
|
||||
)
|
||||
|> post("/inbox", data)
|
||||
|> post("/inbox", Jason.encode!(data))
|
||||
|
||||
assert conn.assigns.valid_signature == false
|
||||
assert "ok" == json_response(conn, 200)
|
||||
|
|
@ -1063,13 +1106,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> put_req_header("date", "Thu, 25 Jul 2024 13:33:31 GMT")
|
||||
|> put_req_header("date", Signature.signed_date())
|
||||
|> put_req_header("digest", "SHA-256=fake-digest")
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
|
||||
)
|
||||
|> post("/inbox", data)
|
||||
|> post("/inbox", Jason.encode!(data))
|
||||
|
||||
assert conn.assigns.valid_signature == false
|
||||
assert "ok" == json_response(conn, 200)
|
||||
|
|
@ -1829,6 +1872,30 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
assert Activity.get_by_ap_id(data["id"])
|
||||
end
|
||||
|
||||
test "it accepts activities that do not address the inbox owner", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
actor = insert(:user, local: false)
|
||||
|
||||
data = %{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"type" => "Follow",
|
||||
"id" => "#{actor.ap_id}/follows/1",
|
||||
"actor" => actor.ap_id,
|
||||
"object" => user.ap_id,
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"]
|
||||
}
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> assign_valid_signature_for_actor(data["actor"])
|
||||
|> 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 returns an error when receiving an activity sent to a deactivated user", %{
|
||||
conn: conn,
|
||||
data: data
|
||||
|
|
@ -2642,6 +2709,43 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
|
||||
assert result == "Character limit (5 characters) exceeded, contains 11 characters"
|
||||
end
|
||||
|
||||
test "it is rate limited", %{conn: conn, activity: activity} do
|
||||
clear_config([:rate_limit, :statuses_actions], {60_000, 2})
|
||||
user = insert(:user)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> assign(:user, user)
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|
||||
for _ <- 1..2 do
|
||||
conn
|
||||
|> post("/users/#{user.nickname}/outbox", activity)
|
||||
|> json_response(201)
|
||||
end
|
||||
|
||||
assert conn
|
||||
|> post("/users/#{user.nickname}/outbox", activity)
|
||||
|> json_response(429) == %{"error" => "Throttled"}
|
||||
end
|
||||
|
||||
test "it shares the rate limit with the Mastodon API", %{activity: activity} do
|
||||
clear_config([:rate_limit, :statuses_actions], {60_000, 2})
|
||||
%{user: user, conn: conn} = oauth_access(["write"])
|
||||
|
||||
for _ <- 1..2 do
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/v1/statuses", %{status: "cofe"})
|
||||
|> json_response(200)
|
||||
end
|
||||
|
||||
assert conn
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> post("/users/#{user.nickname}/outbox", activity)
|
||||
|> json_response(429) == %{"error" => "Throttled"}
|
||||
end
|
||||
end
|
||||
|
||||
describe "/relay/followers" do
|
||||
|
|
@ -3104,4 +3208,125 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
obj_id in object_ids
|
||||
end)
|
||||
end
|
||||
|
||||
describe "OAuth scopes" do
|
||||
setup do
|
||||
Mox.stub_with(Pleroma.StaticStubbedConfigMock, Pleroma.Config)
|
||||
[user: insert(:user)]
|
||||
end
|
||||
|
||||
defp with_token(conn, user, scopes) do
|
||||
token = insert(:oauth_token, user: user, scopes: scopes)
|
||||
|
||||
conn
|
||||
|> put_req_header("authorization", "Bearer #{token.token}")
|
||||
|> put_req_header("accept", "application/activity+json")
|
||||
end
|
||||
|
||||
test "GET /api/ap/whoami needs read:accounts", %{conn: conn, user: user} do
|
||||
assert conn |> with_token(user, ["profile"]) |> get("/api/ap/whoami") |> json_response(403)
|
||||
|
||||
assert %{"id" => id} =
|
||||
conn
|
||||
|> with_token(user, ["read:accounts"])
|
||||
|> get("/api/ap/whoami")
|
||||
|> json_response(200)
|
||||
|
||||
assert id == user.ap_id
|
||||
end
|
||||
|
||||
test "GET /users/:nickname/inbox needs read:statuses", %{conn: conn, user: user} do
|
||||
inbox = "/users/#{user.nickname}/inbox"
|
||||
|
||||
assert conn |> with_token(user, ["read:accounts"]) |> get(inbox) |> json_response(403)
|
||||
assert conn |> with_token(user, ["read:statuses"]) |> get(inbox) |> json_response(200)
|
||||
end
|
||||
|
||||
test "POST /users/:nickname/outbox needs write", %{conn: conn, user: user} do
|
||||
activity = %{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"type" => "Create",
|
||||
"object" => %{
|
||||
"type" => "Note",
|
||||
"content" => "scoped",
|
||||
"to" => "https://www.w3.org/ns/activitystreams#Public",
|
||||
"cc" => []
|
||||
}
|
||||
}
|
||||
|
||||
post = fn scopes ->
|
||||
conn
|
||||
|> with_token(user, scopes)
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|> post("/users/#{user.nickname}/outbox", activity)
|
||||
end
|
||||
|
||||
assert post.(["profile", "write:media"]) |> json_response(403)
|
||||
assert post.(["write"]) |> json_response(201)
|
||||
end
|
||||
|
||||
test "POST /api/ap/upload_media is forbidden without write:media", %{conn: conn, user: user} do
|
||||
assert conn
|
||||
|> with_token(user, ["write:statuses"])
|
||||
|> post("/api/ap/upload_media", %{})
|
||||
|> json_response(403)
|
||||
end
|
||||
|
||||
# majic's libmagic port is unavailable on local Darwin runs; Linux CI still runs this test.
|
||||
@tag :skip_darwin
|
||||
test "POST /api/ap/upload_media works with write:media", %{conn: conn, user: user} do
|
||||
image = %Plug.Upload{
|
||||
content_type: "image/jpeg",
|
||||
path: Path.absname("test/fixtures/image.jpg"),
|
||||
filename: "an_image.jpg"
|
||||
}
|
||||
|
||||
assert conn
|
||||
|> with_token(user, ["write:media"])
|
||||
|> post("/api/ap/upload_media", %{"file" => image})
|
||||
|> json_response(201)
|
||||
end
|
||||
|
||||
test "GET /users/:nickname/outbox shows private posts only with read:statuses",
|
||||
%{conn: conn, user: user} do
|
||||
{:ok, activity} = CommonAPI.post(user, %{status: "secret", visibility: "private"})
|
||||
outbox = "/users/#{user.nickname}/outbox?page=true"
|
||||
|
||||
ids = fn scopes ->
|
||||
conn
|
||||
|> with_token(user, scopes)
|
||||
|> get(outbox)
|
||||
|> json_response(200)
|
||||
|> Map.fetch!("orderedItems")
|
||||
|> Enum.map(& &1["id"])
|
||||
end
|
||||
|
||||
refute activity.data["id"] in ids.(["profile"])
|
||||
assert activity.data["id"] in ids.(["read:statuses"])
|
||||
end
|
||||
|
||||
test "GET /users/:nickname/followers shows hidden followers only with read:accounts",
|
||||
%{conn: conn} do
|
||||
user = insert(:user, hide_followers: true)
|
||||
User.follow(insert(:user), user)
|
||||
followers = "/users/#{user.nickname}/followers?page=1"
|
||||
|
||||
assert conn |> with_token(user, ["profile"]) |> get(followers) |> response(403)
|
||||
|
||||
assert %{"orderedItems" => [_]} =
|
||||
conn |> with_token(user, ["read:accounts"]) |> get(followers) |> json_response(200)
|
||||
end
|
||||
|
||||
test "GET /users/:nickname/following shows hidden follows only with read:accounts",
|
||||
%{conn: conn} do
|
||||
user = insert(:user, hide_follows: true)
|
||||
User.follow(user, insert(:user))
|
||||
following = "/users/#{user.nickname}/following?page=1"
|
||||
|
||||
assert conn |> with_token(user, ["profile"]) |> get(following) |> response(403)
|
||||
|
||||
assert %{"orderedItems" => [_]} =
|
||||
conn |> with_token(user, ["read:accounts"]) |> get(following) |> json_response(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -184,6 +184,34 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
assert user.follower_address == "http://mastodon.example.org/users/admin/followers"
|
||||
end
|
||||
|
||||
test "it records the user's instance" do
|
||||
refute Repo.get_by(Pleroma.Instances.Instance, host: "mastodon.example.org")
|
||||
|
||||
{:ok, _user} = ActivityPub.make_user_from_ap_id("http://mastodon.example.org/users/admin")
|
||||
|
||||
assert %{unreachable_since: nil} =
|
||||
Repo.get_by(Pleroma.Instances.Instance, host: "mastodon.example.org")
|
||||
end
|
||||
|
||||
test "it records the instance of a known user when refreshing it" do
|
||||
insert(:user, local: false, ap_id: "http://mastodon.example.org/users/admin")
|
||||
refute Repo.get_by(Pleroma.Instances.Instance, host: "mastodon.example.org")
|
||||
|
||||
{:ok, _user} = ActivityPub.make_user_from_ap_id("http://mastodon.example.org/users/admin")
|
||||
|
||||
assert Repo.get_by(Pleroma.Instances.Instance, host: "mastodon.example.org")
|
||||
end
|
||||
|
||||
test "it keeps an existing instance record" do
|
||||
unreachable_since = ~N[2020-01-01 00:00:00.000000]
|
||||
insert(:instance, host: "mastodon.example.org", unreachable_since: unreachable_since)
|
||||
|
||||
{:ok, _user} = ActivityPub.make_user_from_ap_id("http://mastodon.example.org/users/admin")
|
||||
|
||||
assert %{unreachable_since: ^unreachable_since} =
|
||||
Repo.get_by(Pleroma.Instances.Instance, host: "mastodon.example.org")
|
||||
end
|
||||
|
||||
test "it returns a user that is invisible" do
|
||||
user_id = "http://mastodon.example.org/users/relay"
|
||||
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
|
||||
|
|
@ -244,6 +272,58 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
}
|
||||
end
|
||||
|
||||
test "rejects actors whose inbox is on another host" do
|
||||
ap_id = "https://example.com/users/evil"
|
||||
|
||||
user_data =
|
||||
"test/fixtures/users_mock/user.json"
|
||||
|> File.read!()
|
||||
|> String.replace("{{nickname}}", "evil")
|
||||
|> Jason.decode!()
|
||||
|> Map.put("inbox", "https://victim.example/inbox")
|
||||
|> Map.put("featured", nil)
|
||||
|> Jason.encode!()
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{method: :get, url: ^ap_id} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: user_data,
|
||||
headers: [{"content-type", "application/activity+json"}]
|
||||
}
|
||||
end)
|
||||
|
||||
assert {:error, _} = ActivityPub.make_user_from_ap_id(ap_id)
|
||||
refute User.get_by_ap_id(ap_id)
|
||||
end
|
||||
|
||||
test "updates the known user when fetched from a display URL" do
|
||||
ap_id = "https://example.com/users/lain"
|
||||
display_url = "https://example.com/@lain"
|
||||
user = insert(:user, local: false, ap_id: ap_id, nickname: "lain@example.com")
|
||||
|
||||
user_data =
|
||||
"test/fixtures/users_mock/user.json"
|
||||
|> File.read!()
|
||||
|> String.replace("{{nickname}}", "lain")
|
||||
|> Jason.decode!()
|
||||
|> Map.merge(%{"featured" => nil, "name" => "new name"})
|
||||
|> Jason.encode!()
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: url} when url in [ap_id, display_url] ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: user_data,
|
||||
headers: [{"content-type", "application/activity+json"}]
|
||||
}
|
||||
end)
|
||||
|
||||
assert {:ok, %User{id: id, name: "new name"}} =
|
||||
ActivityPub.make_user_from_ap_id(display_url)
|
||||
|
||||
assert id == user.id
|
||||
end
|
||||
|
||||
test "fetches user featured collection" do
|
||||
ap_id = "https://example.com/users/lain"
|
||||
|
||||
|
|
@ -516,7 +596,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
end
|
||||
|
||||
test "fetches user birthday information from misskey" do
|
||||
user_id = "https://misskey.io/@mkljczk"
|
||||
user_id = "https://misskey.io/users/8dhi2ne167"
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{
|
||||
|
|
@ -2033,7 +2113,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
json(
|
||||
%{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"id" => "https://remote.org/users/masto_hidden_counters/followers"
|
||||
"id" => "https://remote.org/users/masto_hidden_counters/following"
|
||||
},
|
||||
headers: HttpRequestMock.activitypub_object_headers()
|
||||
)
|
||||
|
|
@ -2045,7 +2125,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
json(
|
||||
%{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"id" => "https://remote.org/users/masto_hidden_counters/following"
|
||||
"id" => "https://remote.org/users/masto_hidden_counters/followers"
|
||||
},
|
||||
headers: HttpRequestMock.activitypub_object_headers()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicyTest do
|
|||
alias Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
setup do:
|
||||
clear_config(:mrf_object_age,
|
||||
threshold: 172_800,
|
||||
|
|
@ -79,7 +81,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicyTest do
|
|||
|
||||
{:ok, data} = ObjectAgePolicy.filter(data)
|
||||
|
||||
assert Visibility.get_visibility(%{data: data}) == "unlisted"
|
||||
assert Visibility.get_visibility(%{data: data}) == "direct"
|
||||
end
|
||||
|
||||
test "it delists an old post" do
|
||||
|
|
@ -94,6 +96,90 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicyTest do
|
|||
assert Visibility.get_visibility(%{data: data}) == "unlisted"
|
||||
end
|
||||
|
||||
test "it keeps an old followers-only post private" do
|
||||
clear_config([:mrf_object_age, :actions], [:delist])
|
||||
|
||||
data = get_old_message()
|
||||
{:ok, user} = User.get_or_fetch_by_ap_id(data["actor"])
|
||||
|
||||
data =
|
||||
data
|
||||
|> Map.put("to", [user.follower_address])
|
||||
|> Map.put("cc", [])
|
||||
|> put_in(["object", "to"], [user.follower_address])
|
||||
|> put_in(["object", "cc"], [])
|
||||
|
||||
{:ok, data} = ObjectAgePolicy.filter(data)
|
||||
|
||||
assert data["to"] == [user.follower_address]
|
||||
assert data["cc"] == []
|
||||
assert Visibility.get_visibility(%{data: data}) == "private"
|
||||
assert data["object"]["to"] == data["to"]
|
||||
assert data["object"]["cc"] == data["cc"]
|
||||
end
|
||||
|
||||
test "it keeps an old direct message direct" do
|
||||
clear_config([:mrf_object_age, :actions], [:delist])
|
||||
|
||||
data = get_old_message()
|
||||
{:ok, _user} = User.get_or_fetch_by_ap_id(data["actor"])
|
||||
recipient = "https://example.com/users/someone"
|
||||
|
||||
data =
|
||||
data
|
||||
|> Map.put("to", [recipient])
|
||||
|> Map.put("cc", [])
|
||||
|> put_in(["object", "to"], [recipient])
|
||||
|> put_in(["object", "cc"], [])
|
||||
|
||||
{:ok, data} = ObjectAgePolicy.filter(data)
|
||||
|
||||
assert data["to"] == [recipient]
|
||||
assert data["cc"] == []
|
||||
assert Visibility.get_visibility(%{data: data}) == "direct"
|
||||
end
|
||||
|
||||
test "it removes every copy of the follower address" do
|
||||
clear_config([:mrf_object_age, :actions], [:strip_followers])
|
||||
|
||||
data = get_old_message()
|
||||
{:ok, user} = User.get_or_fetch_by_ap_id(data["actor"])
|
||||
public = Pleroma.Constants.as_public()
|
||||
followers = [user.follower_address, user.follower_address]
|
||||
|
||||
data =
|
||||
data
|
||||
|> Map.put("to", [public])
|
||||
|> Map.put("cc", followers)
|
||||
|> put_in(["object", "to"], [public])
|
||||
|> put_in(["object", "cc"], followers)
|
||||
|
||||
{:ok, data} = ObjectAgePolicy.filter(data)
|
||||
|
||||
assert data["to"] == [public]
|
||||
assert data["cc"] == []
|
||||
end
|
||||
|
||||
test "it does not duplicate addresses of an old unlisted post" do
|
||||
clear_config([:mrf_object_age, :actions], [:delist, :strip_followers])
|
||||
|
||||
data = get_old_message()
|
||||
{:ok, user} = User.get_or_fetch_by_ap_id(data["actor"])
|
||||
public = Pleroma.Constants.as_public()
|
||||
|
||||
data =
|
||||
data
|
||||
|> Map.put("to", [user.follower_address])
|
||||
|> Map.put("cc", [public])
|
||||
|> put_in(["object", "to"], [user.follower_address])
|
||||
|> put_in(["object", "cc"], [public])
|
||||
|
||||
{:ok, data} = ObjectAgePolicy.filter(data)
|
||||
|
||||
assert data["to"] == []
|
||||
assert data["cc"] == [public]
|
||||
end
|
||||
|
||||
test "it allows a new post" do
|
||||
clear_config([:mrf_object_age, :actions], [:delist])
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
|
|||
use ExUnit.Case
|
||||
use Pleroma.Tests.Helpers
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
|
||||
alias Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
defp regexes_match!([], []), do: true
|
||||
|
|
@ -21,14 +19,14 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
|
|||
test "subdomains_regex/1" do
|
||||
regexes = MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"])
|
||||
|
||||
assert regexes_match!(regexes, [~r/^unsafe.tld$/i, ~r/^(.*\.)*unsafe.tld$/i])
|
||||
assert regexes_match!(regexes, [~r/^unsafe\.tld$/i, ~r/^(.*\.)?unsafe\.tld$/i])
|
||||
end
|
||||
|
||||
describe "subdomain_match/2" do
|
||||
test "common domains" do
|
||||
regexes = MRF.subdomains_regex(["unsafe.tld", "unsafe2.tld"])
|
||||
|
||||
assert regexes_match!(regexes, [~r/^unsafe.tld$/i, ~r/^unsafe2.tld$/i])
|
||||
assert regexes_match!(regexes, [~r/^unsafe\.tld$/i, ~r/^unsafe2\.tld$/i])
|
||||
|
||||
assert MRF.subdomain_match?(regexes, "unsafe.tld")
|
||||
assert MRF.subdomain_match?(regexes, "unsafe2.tld")
|
||||
|
|
@ -39,7 +37,7 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
|
|||
test "wildcard domains with one subdomain" do
|
||||
regexes = MRF.subdomains_regex(["*.unsafe.tld"])
|
||||
|
||||
assert regexes_match!(regexes, [~r/^(.*\.)*unsafe.tld$/i])
|
||||
assert regexes_match!(regexes, [~r/^(.*\.)?unsafe\.tld$/i])
|
||||
|
||||
assert MRF.subdomain_match?(regexes, "unsafe.tld")
|
||||
assert MRF.subdomain_match?(regexes, "sub.unsafe.tld")
|
||||
|
|
@ -50,7 +48,7 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
|
|||
test "wildcard domains with two subdomains" do
|
||||
regexes = MRF.subdomains_regex(["*.unsafe.tld"])
|
||||
|
||||
assert regexes_match!(regexes, [~r/^(.*\.)*unsafe.tld$/i])
|
||||
assert regexes_match!(regexes, [~r/^(.*\.)?unsafe\.tld$/i])
|
||||
|
||||
assert MRF.subdomain_match?(regexes, "unsafe.tld")
|
||||
assert MRF.subdomain_match?(regexes, "sub.sub.unsafe.tld")
|
||||
|
|
@ -61,7 +59,7 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
|
|||
test "matches are case-insensitive" do
|
||||
regexes = MRF.subdomains_regex(["UnSafe.TLD", "UnSAFE2.Tld"])
|
||||
|
||||
assert regexes_match!(regexes, [~r/^UnSafe.TLD$/i, ~r/^UnSAFE2.Tld$/i])
|
||||
assert regexes_match!(regexes, [~r/^UnSafe\.TLD$/i, ~r/^UnSAFE2\.Tld$/i])
|
||||
|
||||
assert MRF.subdomain_match?(regexes, "UNSAFE.TLD")
|
||||
assert MRF.subdomain_match?(regexes, "UNSAFE2.TLD")
|
||||
|
|
@ -72,11 +70,20 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
|
|||
refute MRF.subdomain_match?(regexes, "example.com")
|
||||
end
|
||||
|
||||
test "logs sensible error on accidental wildcard" do
|
||||
assert_raise Regex.CompileError, fn ->
|
||||
assert capture_log(MRF.subdomains_regex(["*unsafe.tld"])) =~
|
||||
"MRF: Invalid subdomain Regex: *unsafe.tld"
|
||||
end
|
||||
test "dots only match dots" do
|
||||
regexes = MRF.subdomains_regex(["unsafe.tld", "*.other.tld"])
|
||||
|
||||
refute MRF.subdomain_match?(regexes, "unsafextld")
|
||||
refute MRF.subdomain_match?(regexes, "otherxtld")
|
||||
refute MRF.subdomain_match?(regexes, "sub.otherxtld")
|
||||
end
|
||||
|
||||
test "accidental wildcards and regex syntax match nothing" do
|
||||
regexes = MRF.subdomains_regex(["*unsafe.tld", "bad(.tld"])
|
||||
|
||||
refute MRF.subdomain_match?(regexes, "unsafe.tld")
|
||||
refute MRF.subdomain_match?(regexes, "sub.unsafe.tld")
|
||||
refute MRF.subdomain_match?(regexes, "bad.tld")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -139,4 +146,37 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
|
|||
type: :group
|
||||
}
|
||||
end
|
||||
|
||||
describe "instance_allowed?/1" do
|
||||
setup do: clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
|
||||
setup do: clear_config([:mrf_simple, :reject], [{"rejected.example", "spam"}])
|
||||
setup do: clear_config([:mrf_simple, :accept], [])
|
||||
|
||||
test "is false for hosts rejected by SimplePolicy, including subdomains" do
|
||||
clear_config([:mrf_simple, :reject], [{"*.rejected.example", "spam"}])
|
||||
|
||||
refute MRF.instance_allowed?("rejected.example")
|
||||
refute MRF.instance_allowed?("sub.rejected.example")
|
||||
assert MRF.instance_allowed?("fine.example")
|
||||
end
|
||||
|
||||
test "only allows accepted hosts when SimplePolicy has an accept list" do
|
||||
clear_config([:mrf_simple, :accept], [{"friend.example", ""}])
|
||||
|
||||
assert MRF.instance_allowed?("friend.example")
|
||||
refute MRF.instance_allowed?("stranger.example")
|
||||
end
|
||||
|
||||
test "ignores the SimplePolicy lists when the policy is not enabled" do
|
||||
clear_config([:mrf, :policies], [])
|
||||
clear_config([:mrf_simple, :accept], [{"friend.example", ""}])
|
||||
|
||||
assert MRF.instance_allowed?("rejected.example")
|
||||
assert MRF.instance_allowed?("stranger.example")
|
||||
end
|
||||
|
||||
test "is false without a host" do
|
||||
refute MRF.instance_allowed?(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -216,6 +216,39 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
|
|||
assert content =~ "@uncached"
|
||||
end
|
||||
|
||||
test "a Misskey MFM remote mention is not rendered as an email link" do
|
||||
remote_user = insert(:user, ap_id: "https://mk.absturztau.be/users/alice")
|
||||
mentioned_user = insert(:user, ap_id: "https://movsw.0x0.st/users/bob")
|
||||
|
||||
note = %{
|
||||
"id" => "https://mk.absturztau.be/notes/1",
|
||||
"type" => "Note",
|
||||
"actor" => remote_user.ap_id,
|
||||
"attributedTo" => remote_user.ap_id,
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => [mentioned_user.ap_id],
|
||||
"content" => ~s(<p>@<a href="mailto:bob@movsw.0x0.st">bob@movsw.0x0.st</a> hello</p>),
|
||||
"context" => Utils.generate_context_id(),
|
||||
"tag" => [
|
||||
%{
|
||||
"type" => "Mention",
|
||||
"name" => "@bob@movsw.0x0.st",
|
||||
"href" => mentioned_user.ap_id
|
||||
}
|
||||
],
|
||||
"source" => %{
|
||||
"content" => "@bob@movsw.0x0.st hello",
|
||||
"mediaType" => "text/x.misskeymarkdown"
|
||||
}
|
||||
}
|
||||
|
||||
%{valid?: true, changes: %{content: content}} =
|
||||
ArticleNotePageValidator.cast_and_validate(note)
|
||||
|
||||
assert content =~ ~s(href="#{mentioned_user.ap_id}")
|
||||
refute content =~ "mailto:"
|
||||
end
|
||||
|
||||
test "a Misskey MFM note drops oversized source content instead of parsing it" do
|
||||
user = insert(:user, ap_id: "https://misskey.example/users/oversized")
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.UserValidatorTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.UserValidator
|
||||
|
||||
defp actor do
|
||||
"test/fixtures/users_mock/user.json"
|
||||
|> File.read!()
|
||||
|> String.replace("{{nickname}}", "lain")
|
||||
|> Jason.decode!()
|
||||
end
|
||||
|
||||
test "accepts an actor whose endpoints and collections are on its own host" do
|
||||
data = actor()
|
||||
assert {:ok, ^data, []} = UserValidator.validate(data, [])
|
||||
end
|
||||
|
||||
test "accepts an actor without optional fields" do
|
||||
data = Map.drop(actor(), ["endpoints", "featured", "followers", "following", "publicKey"])
|
||||
assert {:ok, _, _} = UserValidator.validate(data, [])
|
||||
end
|
||||
|
||||
test "accepts all actor fixtures" do
|
||||
for path <- Path.wildcard("test/fixtures/**/*.json"),
|
||||
{:ok, %{"inbox" => _, "id" => id} = data} when is_binary(id) <-
|
||||
[Jason.decode(File.read!(path))] do
|
||||
assert {{:ok, _, _}, ^path} = {UserValidator.validate(data, []), path}
|
||||
end
|
||||
end
|
||||
|
||||
test "treats empty and null fields as missing" do
|
||||
data =
|
||||
actor()
|
||||
|> Map.merge(%{"inbox" => "", "followers" => nil, "following" => "", "featured" => ""})
|
||||
|> put_in(["endpoints", "sharedInbox"], "")
|
||||
|> put_in(["publicKey", "id"], "")
|
||||
|
||||
assert {:ok, _, _} = UserValidator.validate(data, [])
|
||||
end
|
||||
|
||||
for field <- ["inbox", "followers", "following", "featured"] do
|
||||
test "rejects an actor whose #{field} is on another host" do
|
||||
data = Map.put(actor(), unquote(field), "https://victim.example/users/lain/x")
|
||||
assert {:error, _} = UserValidator.validate(data, [])
|
||||
end
|
||||
end
|
||||
|
||||
test "rejects an actor whose sharedInbox is on another host" do
|
||||
data = put_in(actor(), ["endpoints", "sharedInbox"], "https://victim.example/inbox")
|
||||
assert {:error, _} = UserValidator.validate(data, [])
|
||||
end
|
||||
|
||||
test "rejects an actor whose collection is an object on another host" do
|
||||
data = Map.put(actor(), "followers", %{"id" => "https://victim.example/users/v/followers"})
|
||||
assert {:error, _} = UserValidator.validate(data, [])
|
||||
end
|
||||
|
||||
test "rejects an actor whose key id is on another host" do
|
||||
data = put_in(actor(), ["publicKey", "id"], "https://victim.example/users/lain#main-key")
|
||||
assert {:error, _} = UserValidator.validate(data, [])
|
||||
end
|
||||
|
||||
test "rejects an actor whose key id has no host" do
|
||||
data = put_in(actor(), ["publicKey", "id"], "lain")
|
||||
assert {:error, _} = UserValidator.validate(data, [])
|
||||
end
|
||||
|
||||
test "rejects an actor without an id" do
|
||||
assert {:error, _} = UserValidator.validate(Map.delete(actor(), "id"), [])
|
||||
end
|
||||
end
|
||||
|
|
@ -32,8 +32,26 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
|
|||
refute Publisher.should_federate?(nil, true)
|
||||
end
|
||||
|
||||
test "it returns true when public is true" do
|
||||
assert Publisher.should_federate?(false, true)
|
||||
test "it returns true for public activities to quarantined instances" do
|
||||
clear_config([:instance, :quarantined_instances], [{"quarantined.example", ""}])
|
||||
|
||||
assert Publisher.should_federate?("https://quarantined.example/inbox", true)
|
||||
refute Publisher.should_federate?("https://quarantined.example/inbox", false)
|
||||
end
|
||||
|
||||
test "it returns false for instances rejected by SimplePolicy, even for public activities" do
|
||||
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
|
||||
clear_config([:mrf_simple, :reject], [{"rejected.example", ""}])
|
||||
|
||||
refute Publisher.should_federate?("https://rejected.example/inbox", true)
|
||||
refute Publisher.should_federate?("https://rejected.example/inbox", false)
|
||||
assert Publisher.should_federate?("https://fine.example/inbox", true)
|
||||
end
|
||||
|
||||
test "it returns false for inboxes without a host" do
|
||||
clear_config([:instance, :quarantined_instances], [{"quarantined.example", ""}])
|
||||
|
||||
refute Publisher.should_federate?("inbox", false)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -119,7 +137,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
|
|||
assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
|
||||
end
|
||||
|
||||
test "it returns inbox for messages involving single recipients in total" do
|
||||
test "it returns sharedInbox for messages to a single recipient" do
|
||||
user =
|
||||
insert(:user, %{
|
||||
shared_inbox: "http://example.com/inbox",
|
||||
|
|
@ -130,6 +148,46 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
|
|||
data: %{"to" => [user.ap_id], "cc" => []}
|
||||
}
|
||||
|
||||
assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
|
||||
end
|
||||
|
||||
test "it returns sharedInbox for followers-only messages" do
|
||||
user =
|
||||
insert(:user, %{
|
||||
shared_inbox: "http://example.com/inbox",
|
||||
inbox: "http://example.com/personal-inbox"
|
||||
})
|
||||
|
||||
author = insert(:user)
|
||||
|
||||
activity = %Activity{
|
||||
data: %{"to" => [author.follower_address], "cc" => []}
|
||||
}
|
||||
|
||||
assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
|
||||
end
|
||||
|
||||
test "it returns inbox for messages with bcc or bto, which needs personal inboxes" do
|
||||
user =
|
||||
insert(:user, %{
|
||||
shared_inbox: "http://example.com/inbox",
|
||||
inbox: "http://example.com/personal-inbox"
|
||||
})
|
||||
|
||||
for field <- ["bcc", "bto"] do
|
||||
activity = %Activity{
|
||||
data: %{"to" => [@as_public], "cc" => [], field => [user.ap_id]}
|
||||
}
|
||||
|
||||
assert Publisher.determine_inbox(activity, user) == "http://example.com/personal-inbox"
|
||||
end
|
||||
end
|
||||
|
||||
test "it returns inbox when the user has no sharedInbox" do
|
||||
user = insert(:user, %{shared_inbox: nil, inbox: "http://example.com/personal-inbox"})
|
||||
|
||||
activity = %Activity{data: %{"to" => [@as_public], "cc" => []}}
|
||||
|
||||
assert Publisher.determine_inbox(activity, user) == "http://example.com/personal-inbox"
|
||||
end
|
||||
end
|
||||
|
|
@ -204,6 +262,32 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
|
|||
)
|
||||
end
|
||||
|
||||
test "doesn't publish a public activity to instances rejected by SimplePolicy" do
|
||||
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
|
||||
clear_config([:mrf_simple, :reject], [{"rejected.example", "spam"}])
|
||||
|
||||
actor = insert(:user)
|
||||
|
||||
for host <- ["rejected.example", "fine.example"] do
|
||||
follower = insert(:user, local: false, inbox: "https://#{host}/users/nick/inbox")
|
||||
{:ok, _, _} = Pleroma.User.follow(follower, actor)
|
||||
end
|
||||
|
||||
actor = refresh_record(actor)
|
||||
|
||||
note_activity =
|
||||
insert(:note_activity, user: actor, recipients: [@as_public, actor.follower_address])
|
||||
|
||||
assert :ok == Publisher.publish(actor, note_activity)
|
||||
|
||||
inboxes =
|
||||
all_enqueued(worker: "Pleroma.Workers.PublisherWorker")
|
||||
|> Enum.map(& &1.args["params"]["inbox"])
|
||||
|
||||
assert "https://fine.example/users/nick/inbox" in inboxes
|
||||
refute "https://rejected.example/users/nick/inbox" in inboxes
|
||||
end
|
||||
|
||||
test "Publishes a non-public activity to non-quarantined instances." do
|
||||
Config.put([:instance, :quarantined_instances], [{"somedomain.com", "some reason"}])
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,31 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do
|
|||
)
|
||||
end
|
||||
|
||||
test "it works for incoming custom emoji reactions with a single tag object" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user, local: false)
|
||||
{:ok, activity} = CommonAPI.post(user, %{status: "hello"})
|
||||
|
||||
data =
|
||||
File.read!("test/fixtures/custom-emoji-reaction.json")
|
||||
|> Jason.decode!()
|
||||
|> Map.put("object", activity.data["object"])
|
||||
|> Map.put("actor", other_user.ap_id)
|
||||
|
||||
[tag] = data["tag"]
|
||||
data = Map.put(data, "tag", tag)
|
||||
|
||||
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
|
||||
|
||||
assert data["content"] == ":hanapog:"
|
||||
assert [%{"name" => "hanapog"}] = data["tag"]
|
||||
|
||||
object = Object.get_by_ap_id(data["object"])
|
||||
url = tag["icon"]["url"]
|
||||
|
||||
assert match?([["hanapog", _, ^url]], object.data["reactions"])
|
||||
end
|
||||
|
||||
test "it works for incoming unqualified emoji reactions" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user, local: false)
|
||||
|
|
|
|||
|
|
@ -103,6 +103,25 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.QuestionHandlingTest do
|
|||
assert Enum.sort(object.data["oneOf"]) == Enum.sort(options)
|
||||
end
|
||||
|
||||
test "Question activity with a single, non-array tag" do
|
||||
tag = %{
|
||||
"href" => "https://gts.example/tags/caturday",
|
||||
"name" => "#caturday",
|
||||
"type" => "Hashtag"
|
||||
}
|
||||
|
||||
data =
|
||||
File.read!("test/fixtures/mastodon-question-activity.json")
|
||||
|> Jason.decode!()
|
||||
|> Kernel.put_in(["object", "tag"], tag)
|
||||
|
||||
{:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
|
||||
assert tag in object.data["tag"]
|
||||
assert Object.hashtags(object) == ["caturday"]
|
||||
end
|
||||
|
||||
test "Mastodon Question activity with custom emojis" do
|
||||
options = [
|
||||
%{
|
||||
|
|
|
|||
|
|
@ -11,8 +11,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.UserUpdateHandlingTest do
|
|||
|
||||
import Pleroma.Factory
|
||||
|
||||
# matches the host of the actor in mastodon-update.json
|
||||
@gargron "http://mastodon.example.org/users/gargron"
|
||||
|
||||
test "it works for incoming update activities" do
|
||||
user = insert(:user, local: false)
|
||||
user = insert(:user, local: false, ap_id: @gargron)
|
||||
|
||||
update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
|
||||
|
||||
|
|
@ -50,8 +53,28 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.UserUpdateHandlingTest do
|
|||
assert user.bio == "<p>Some bio</p>"
|
||||
end
|
||||
|
||||
test "it rejects updates that move the shared inbox to another host" do
|
||||
user = insert(:user, local: false, ap_id: @gargron)
|
||||
|
||||
update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
|
||||
|
||||
object =
|
||||
update_data["object"]
|
||||
|> Map.put("actor", user.ap_id)
|
||||
|> Map.put("id", user.ap_id)
|
||||
|> put_in(["endpoints", "sharedInbox"], "https://victim.example/inbox")
|
||||
|
||||
update_data =
|
||||
update_data
|
||||
|> Map.put("actor", user.ap_id)
|
||||
|> Map.put("object", object)
|
||||
|
||||
assert {:error, _} = Transmogrifier.handle_incoming(update_data)
|
||||
assert User.get_cached_by_ap_id(user.ap_id).shared_inbox == user.shared_inbox
|
||||
end
|
||||
|
||||
test "it selects an actor type after an extension type" do
|
||||
user = insert(:user, local: false)
|
||||
user = insert(:user, local: false, ap_id: @gargron)
|
||||
update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
|
||||
|
||||
object =
|
||||
|
|
@ -70,7 +93,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.UserUpdateHandlingTest do
|
|||
end
|
||||
|
||||
test "it works with alsoKnownAs" do
|
||||
%{ap_id: actor} = insert(:user, local: false)
|
||||
%{ap_id: actor} = insert(:user, local: false, ap_id: @gargron)
|
||||
|
||||
assert User.get_cached_by_ap_id(actor).also_known_as == []
|
||||
|
||||
|
|
@ -97,7 +120,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.UserUpdateHandlingTest do
|
|||
end
|
||||
|
||||
test "it works with custom profile fields" do
|
||||
user = insert(:user, local: false)
|
||||
user = insert(:user, local: false, ap_id: @gargron)
|
||||
|
||||
assert user.fields == []
|
||||
|
||||
|
|
@ -155,7 +178,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.UserUpdateHandlingTest do
|
|||
end
|
||||
|
||||
test "it works for incoming update activities which lock the account" do
|
||||
user = insert(:user, local: false)
|
||||
user = insert(:user, local: false, ap_id: @gargron)
|
||||
|
||||
update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
|
||||
|
||||
|
|
|
|||
|
|
@ -147,6 +147,53 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
|
|||
assert activity.data["type"] == "Move"
|
||||
end
|
||||
|
||||
test "it refetches a cached remote Move target that lacks the alias" do
|
||||
old_user = insert(:user, local: false, ap_id: "https://old.example/users/old")
|
||||
|
||||
new_user =
|
||||
insert(:user,
|
||||
local: false,
|
||||
ap_id: "https://new.example/users/new",
|
||||
nickname: "new@new.example",
|
||||
last_refreshed_at: NaiveDateTime.utc_now()
|
||||
)
|
||||
|
||||
actor = %{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"id" => new_user.ap_id,
|
||||
"type" => "Person",
|
||||
"preferredUsername" => "new",
|
||||
"inbox" => new_user.ap_id <> "/inbox",
|
||||
"outbox" => new_user.ap_id <> "/outbox",
|
||||
"followers" => new_user.ap_id <> "/followers",
|
||||
"following" => new_user.ap_id <> "/following",
|
||||
"alsoKnownAs" => [old_user.ap_id]
|
||||
}
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{method: :get, url: "https://new.example/users/new"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
headers: [{"content-type", "application/activity+json"}],
|
||||
body: Jason.encode!(actor)
|
||||
}
|
||||
|
||||
_ ->
|
||||
%Tesla.Env{status: 404}
|
||||
end)
|
||||
|
||||
message = %{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"type" => "Move",
|
||||
"actor" => old_user.ap_id,
|
||||
"object" => old_user.ap_id,
|
||||
"target" => new_user.ap_id
|
||||
}
|
||||
|
||||
assert {:ok, %Activity{data: %{"type" => "Move"}}} = Transmogrifier.handle_incoming(message)
|
||||
assert old_user.ap_id in User.get_cached_by_ap_id(new_user.ap_id).also_known_as
|
||||
end
|
||||
|
||||
test "it fixes both the Create and object contexts in a reply" do
|
||||
insert(:user, ap_id: "https://mk.absturztau.be/users/8ozbzjs3o8")
|
||||
insert(:user, ap_id: "https://p.helene.moe/users/helene")
|
||||
|
|
|
|||
|
|
@ -406,74 +406,6 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "recipient_in_message/3" do
|
||||
test "returns true when recipient in `to`" do
|
||||
recipient = insert(:user)
|
||||
actor = insert(:user)
|
||||
assert Utils.recipient_in_message(recipient, actor, %{"to" => recipient.ap_id})
|
||||
|
||||
assert Utils.recipient_in_message(
|
||||
recipient,
|
||||
actor,
|
||||
%{"to" => [recipient.ap_id], "cc" => ""}
|
||||
)
|
||||
end
|
||||
|
||||
test "returns true when recipient in `cc`" do
|
||||
recipient = insert(:user)
|
||||
actor = insert(:user)
|
||||
assert Utils.recipient_in_message(recipient, actor, %{"cc" => recipient.ap_id})
|
||||
|
||||
assert Utils.recipient_in_message(
|
||||
recipient,
|
||||
actor,
|
||||
%{"cc" => [recipient.ap_id], "to" => ""}
|
||||
)
|
||||
end
|
||||
|
||||
test "returns true when recipient in `bto`" do
|
||||
recipient = insert(:user)
|
||||
actor = insert(:user)
|
||||
assert Utils.recipient_in_message(recipient, actor, %{"bto" => recipient.ap_id})
|
||||
|
||||
assert Utils.recipient_in_message(
|
||||
recipient,
|
||||
actor,
|
||||
%{"bcc" => "", "bto" => [recipient.ap_id]}
|
||||
)
|
||||
end
|
||||
|
||||
test "returns true when recipient in `bcc`" do
|
||||
recipient = insert(:user)
|
||||
actor = insert(:user)
|
||||
assert Utils.recipient_in_message(recipient, actor, %{"bcc" => recipient.ap_id})
|
||||
|
||||
assert Utils.recipient_in_message(
|
||||
recipient,
|
||||
actor,
|
||||
%{"bto" => "", "bcc" => [recipient.ap_id]}
|
||||
)
|
||||
end
|
||||
|
||||
test "returns true when message without addresses fields" do
|
||||
recipient = insert(:user)
|
||||
actor = insert(:user)
|
||||
assert Utils.recipient_in_message(recipient, actor, %{"bccc" => recipient.ap_id})
|
||||
|
||||
assert Utils.recipient_in_message(
|
||||
recipient,
|
||||
actor,
|
||||
%{"btod" => "", "bccc" => [recipient.ap_id]}
|
||||
)
|
||||
end
|
||||
|
||||
test "returns false" do
|
||||
recipient = insert(:user)
|
||||
actor = insert(:user)
|
||||
refute Utils.recipient_in_message(recipient, actor, %{"to" => "ap_id"})
|
||||
end
|
||||
end
|
||||
|
||||
describe "lazy_put_activity_defaults/2" do
|
||||
test "returns map with id and published data" do
|
||||
note_activity = insert(:note_activity)
|
||||
|
|
|
|||
|
|
@ -1042,9 +1042,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
|
|||
ObanHelpers.perform_all()
|
||||
|
||||
Pleroma.Emails.UserEmail.account_confirmation_email(first_user)
|
||||
# temporary hackney fix until hackney max_connections bug is fixed
|
||||
# https://git.pleroma.social/pleroma/pleroma/-/issues/2101
|
||||
|> Swoosh.Email.put_private(:hackney_options, ssl_options: [versions: [:"tlsv1.2"]])
|
||||
|> Swoosh.Email.put_private(:hackney_options,
|
||||
ssl_options: Pleroma.HTTP.TLS.ssl_options()
|
||||
)
|
||||
|> assert_email_sent()
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,28 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
assert key2 == inspect(config2.key)
|
||||
end
|
||||
|
||||
test "does not crash on a setting with an unknown module key", %{conn: conn} do
|
||||
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
|
||||
|
||||
Pleroma.Repo.insert_all("config", [
|
||||
%{
|
||||
group: ":pleroma",
|
||||
key: "Pleroma.Nowhere.Module",
|
||||
value: :erlang.term_to_binary(enabled: true),
|
||||
inserted_at: now,
|
||||
updated_at: now
|
||||
}
|
||||
])
|
||||
|
||||
capture_log(fn ->
|
||||
for path <- ["/api/pleroma/admin/config", "/api/pleroma/admin/config?only_db=true"] do
|
||||
conn
|
||||
|> get(path)
|
||||
|> json_response_and_validate_schema(200)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
test "db is added to settings that are in db", %{conn: conn} do
|
||||
_config = insert(:config, key: ":instance", value: [name: "Some name"])
|
||||
|
||||
|
|
|
|||
24
test/pleroma/web/api_spec/cast_and_validate_test.exs
Normal file
24
test/pleroma/web/api_spec/cast_and_validate_test.exs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.CastAndValidateTest do
|
||||
use Pleroma.Web.ConnCase, async: false
|
||||
|
||||
describe "permissive mode" do
|
||||
setup do: clear_config([Pleroma.Web.ApiSpec.CastAndValidate, :strict], false)
|
||||
|
||||
test "drops several unexpected query params", %{conn: conn} do
|
||||
assert conn
|
||||
|> get("/api/v1/timelines/public?foo=1&bar=2&baz=3&local=true")
|
||||
|> json_response_and_validate_schema(200)
|
||||
end
|
||||
|
||||
test "still rejects an invalid known param", %{conn: conn} do
|
||||
assert %{"error" => _} =
|
||||
conn
|
||||
|> get("/api/v1/timelines/public?foo=1&bar=2&limit=lots")
|
||||
|> json_response(400)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -125,6 +125,14 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
|
|||
assert output == expected
|
||||
end
|
||||
|
||||
test "MFM keeps bare URLs containing Markdown delimiters intact" do
|
||||
url = "https://example.org/a*b*c"
|
||||
|
||||
{output, [], []} = Utils.format_input(url, "text/x.misskeymarkdown")
|
||||
|
||||
assert output == ~s(<p><a href="#{url}">#{url}</a></p>)
|
||||
end
|
||||
|
||||
test "works for bare text/bbcode" do
|
||||
text = "[b]hello world[/b]"
|
||||
expected = "<strong>hello world</strong>"
|
||||
|
|
@ -220,6 +228,15 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
|
|||
assert result == ~s[<a href="http://example.org/">OwO</a>]
|
||||
end
|
||||
|
||||
test "markdown links do not allow injected attributes" do
|
||||
code = ~s|[click](http://example.com/?a=x" onerror="alert(1))|
|
||||
{result, [], []} = Utils.format_input(code, "text/markdown")
|
||||
|
||||
assert result =~ ~s|<a href="http://example.com/?a=x%22 onerror=%22alert(1)">|
|
||||
refute result =~ ~s|onerror="alert(1)|
|
||||
refute result =~ ~s(<a href="http://example.com/?a=x" onerror=)
|
||||
end
|
||||
|
||||
test "rulers" do
|
||||
code = ~s[before\n\n-----\n\nafter]
|
||||
{result, [], []} = Utils.format_input(code, "text/markdown")
|
||||
|
|
|
|||
|
|
@ -338,6 +338,26 @@ defmodule Pleroma.Web.CommonAPITest do
|
|||
assert {:ok, :no_activity} == CommonAPI.unblock(blocked, blocker)
|
||||
refute User.blocks?(blocker, blocked)
|
||||
end
|
||||
|
||||
test "it unblocks and does not federate if outgoing blocks are disabled" do
|
||||
clear_config([:instance, :federating], true)
|
||||
clear_config([:activitypub, :outgoing_blocks], false)
|
||||
|
||||
blocked = insert(:user)
|
||||
blocker = insert(:user)
|
||||
|
||||
with_mock Pleroma.Web.Federator,
|
||||
publish: fn _ -> nil end do
|
||||
assert {:ok, block} = CommonAPI.block(blocked, blocker)
|
||||
assert block.local
|
||||
assert User.blocks?(blocker, blocked)
|
||||
|
||||
assert {:ok, unblock} = CommonAPI.unblock(blocked, blocker)
|
||||
assert unblock.local
|
||||
refute User.blocks?(blocker, blocked)
|
||||
assert_not_called(Pleroma.Web.Federator.publish(:_))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "deletion" do
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
|
|||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.InternalFetchActor
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
alias Pleroma.Web.Plugs.SetLocalePlug
|
||||
|
||||
|
|
@ -2033,6 +2034,60 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
|
|||
|
||||
assert id == to_string(user.id)
|
||||
end
|
||||
|
||||
test "the profile scope only gets the public account" do
|
||||
user = insert(:user, default_scope: "private", pleroma_settings_store: %{"fe" => %{}})
|
||||
%{conn: conn} = oauth_access(["profile"], user: user)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> get("/api/v1/accounts/verify_credentials")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert response ==
|
||||
AccountView.render("show.json", user: user, skip_visibility_check: true)
|
||||
|> Jason.encode!()
|
||||
|> Jason.decode!()
|
||||
|
||||
refute Map.has_key?(response["source"], "privacy")
|
||||
refute Map.has_key?(response, "follow_requests_count")
|
||||
|
||||
for key <- ~w(email settings_store chat_token notification_settings
|
||||
unread_notifications_count unread_conversation_count allow_following_move) do
|
||||
refute Map.has_key?(response["pleroma"], key), "#{key} must not be shown"
|
||||
end
|
||||
end
|
||||
|
||||
test "the profile scope with read:accounts gets the full account" do
|
||||
%{conn: conn} = oauth_access(["profile", "read:accounts"])
|
||||
|
||||
response =
|
||||
conn
|
||||
|> get("/api/v1/accounts/verify_credentials")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert response["source"]["privacy"]
|
||||
assert response["pleroma"]["chat_token"]
|
||||
end
|
||||
|
||||
test "the profile scope works when unauthenticated users cannot see profiles" do
|
||||
clear_config([:restrict_unauthenticated, :profiles, :local], true)
|
||||
%{user: user, conn: conn} = oauth_access(["profile"])
|
||||
|
||||
assert %{"id" => id} =
|
||||
conn
|
||||
|> get("/api/v1/accounts/verify_credentials")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert id == to_string(user.id)
|
||||
end
|
||||
|
||||
test "the profile scope does not give access to other endpoints" do
|
||||
%{conn: conn} = oauth_access(["profile"])
|
||||
|
||||
assert conn |> get("/api/v1/timelines/home") |> json_response(403)
|
||||
assert conn |> get("/api/v1/follow_requests") |> json_response(403)
|
||||
end
|
||||
end
|
||||
|
||||
describe "user relationships" do
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
|
|||
user2 = insert(:user, %{local: true})
|
||||
{:ok, _user2} = User.set_activation(user2, false)
|
||||
|
||||
insert(:user, %{local: false, nickname: "u@peer1.com"})
|
||||
insert(:user, %{local: false, nickname: "u@peer2.com"})
|
||||
insert(:instance, host: "peer1.com")
|
||||
insert(:instance, host: "peer2.com")
|
||||
|
||||
{:ok, _} = Pleroma.Web.CommonAPI.post(user, %{status: "cofe"})
|
||||
|
||||
|
|
@ -83,8 +83,11 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
|
|||
end
|
||||
|
||||
test "get peers", %{conn: conn} do
|
||||
insert(:user, %{local: false, nickname: "u@peer1.com"})
|
||||
insert(:user, %{local: false, nickname: "u@peer2.com"})
|
||||
# Peers come from the instances table, which remote users add to.
|
||||
insert(:instance, host: "peer1.com")
|
||||
insert(:instance, host: "peer2.com")
|
||||
# Favicon lookups for local accounts can add our own host.
|
||||
insert(:instance, host: Pleroma.Web.Endpoint.host())
|
||||
|
||||
Pleroma.Stats.force_update()
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
|
|||
alias Pleroma.Notification
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
|
|
@ -33,7 +34,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
|
|||
assert response == %{
|
||||
"notifications" => %{
|
||||
"last_read_id" => "69420",
|
||||
"updated_at" => NaiveDateTime.to_iso8601(marker.updated_at),
|
||||
"updated_at" => Utils.to_masto_date(marker.updated_at),
|
||||
"version" => 0,
|
||||
"pleroma" => %{"unread_count" => 7}
|
||||
}
|
||||
|
|
@ -107,7 +108,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
|
|||
assert response == %{
|
||||
"notifications" => %{
|
||||
"last_read_id" => "69888",
|
||||
"updated_at" => NaiveDateTime.to_iso8601(marker.updated_at),
|
||||
"updated_at" => Utils.to_masto_date(marker.updated_at),
|
||||
"version" => 0,
|
||||
"pleroma" => %{"unread_count" => 0}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -328,7 +328,9 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
|
|||
assert status["id"] == to_string(activity.id)
|
||||
end
|
||||
|
||||
test "search fetches remote statuses and prefers them over other results", %{conn: conn} do
|
||||
test "search fetches remote statuses and prefers them over other results" do
|
||||
%{conn: conn} = oauth_access(["read:search"])
|
||||
|
||||
{:ok, %{id: activity_id}} =
|
||||
CommonAPI.post(insert(:user), %{
|
||||
status: "check out http://mastodon.example.org/@admin/99541947525187367"
|
||||
|
|
@ -336,7 +338,9 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
|
|||
|
||||
%{"url" => result_url, "id" => result_id} =
|
||||
conn
|
||||
|> get("/api/v1/search?q=http://mastodon.example.org/@admin/99541947525187367")
|
||||
|> get(
|
||||
"/api/v1/search?resolve=true&q=http://mastodon.example.org/@admin/99541947525187367"
|
||||
)
|
||||
|> json_response_and_validate_schema(200)
|
||||
|> Map.get("statuses")
|
||||
|> List.first()
|
||||
|
|
@ -345,6 +349,28 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
|
|||
assert match?(^result_url, "http://mastodon.example.org/@admin/99541947525187367")
|
||||
end
|
||||
|
||||
test "search fetches remote statuses only with resolve, when logged in, on the first page",
|
||||
%{conn: anonymous} do
|
||||
%{conn: conn} = oauth_access(["read:search"])
|
||||
url = "http://mastodon.example.org/@admin/99541947525187367"
|
||||
|
||||
for {conn, params} <- [
|
||||
{conn, ""},
|
||||
{conn, "&resolve=false"},
|
||||
{conn, "&resolve=true&offset=5"},
|
||||
{anonymous, "&resolve=true"}
|
||||
] do
|
||||
assert %{"statuses" => []} =
|
||||
conn
|
||||
|> get("/api/v1/search?q=#{url}#{params}")
|
||||
|> json_response_and_validate_schema(200)
|
||||
end
|
||||
|
||||
refute Pleroma.Object.get_by_ap_id(
|
||||
"http://mastodon.example.org/users/admin/statuses/99541947525187367"
|
||||
)
|
||||
end
|
||||
|
||||
test "search doesn't show statuses that it shouldn't", %{conn: conn} do
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(insert(:user), %{
|
||||
|
|
|
|||
|
|
@ -2243,6 +2243,31 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|
|||
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
|
||||
end
|
||||
|
||||
describe "status_id_action rate limit" do
|
||||
setup do: clear_config([:rate_limit, :status_id_action], {60_000, 2})
|
||||
setup do: clear_config([:rate_limit, :statuses_actions], {60_000, 100})
|
||||
setup do: oauth_access(["write:statuses", "write:favourites"])
|
||||
|
||||
for {action, undo} <- [{"favourite", "unfavourite"}, {"reblog", "unreblog"}],
|
||||
strict <- [true, false] do
|
||||
test "#{action} is limited per status, not per user (strict: #{strict})", %{conn: conn} do
|
||||
clear_config([Pleroma.Web.ApiSpec.CastAndValidate, :strict], unquote(strict))
|
||||
conn = put_req_header(conn, "content-type", "application/json")
|
||||
statuses = insert_list(3, :note_activity)
|
||||
|
||||
assert Enum.map(
|
||||
statuses,
|
||||
&post(conn, "/api/v1/statuses/#{&1.id}/#{unquote(action)}").status
|
||||
) ==
|
||||
[200, 200, 200]
|
||||
|
||||
status = hd(statuses)
|
||||
assert post(conn, "/api/v1/statuses/#{status.id}/#{unquote(undo)}").status == 200
|
||||
assert post(conn, "/api/v1/statuses/#{status.id}/#{unquote(action)}").status == 429
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "pinned statuses" do
|
||||
setup do: oauth_access(["write:accounts"])
|
||||
|
||||
|
|
@ -3068,6 +3093,30 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|
|||
} = response
|
||||
end
|
||||
|
||||
test "context shows local-only replies to local users only" do
|
||||
poster = insert(:user)
|
||||
replier = insert(:user)
|
||||
%{conn: conn} = oauth_access(["read:statuses"])
|
||||
%{conn: remote_conn} = oauth_access(["read:statuses"], user: insert(:user, local: false))
|
||||
|
||||
{:ok, %{id: id1}} = CommonAPI.post(poster, %{status: "1"})
|
||||
|
||||
{:ok, %{id: id2}} =
|
||||
CommonAPI.post(replier, %{status: "2", visibility: "local", in_reply_to_status_id: id1})
|
||||
|
||||
assert %{"descendants" => [%{"id" => ^id2}]} =
|
||||
conn
|
||||
|> get("/api/v1/statuses/#{id1}/context")
|
||||
|> json_response_and_validate_schema(:ok)
|
||||
|
||||
for conn <- [build_conn(), remote_conn] do
|
||||
assert %{"descendants" => []} =
|
||||
conn
|
||||
|> get("/api/v1/statuses/#{id1}/context")
|
||||
|> json_response_and_validate_schema(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
test "context doesn't leak priv posts" do
|
||||
%{user: user, conn: conn} = oauth_access(["read:statuses"])
|
||||
stranger = insert(:user)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MastodonAPI.MarkerView
|
||||
import Pleroma.Factory
|
||||
|
||||
|
|
@ -14,16 +15,25 @@ defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do
|
|||
assert MarkerView.render("markers.json", %{markers: [marker1, marker2]}) == %{
|
||||
"home" => %{
|
||||
last_read_id: "42",
|
||||
updated_at: NaiveDateTime.to_iso8601(marker2.updated_at),
|
||||
updated_at: Utils.to_masto_date(marker2.updated_at),
|
||||
version: 0,
|
||||
pleroma: %{unread_count: 0}
|
||||
},
|
||||
"notifications" => %{
|
||||
last_read_id: "17",
|
||||
updated_at: NaiveDateTime.to_iso8601(marker1.updated_at),
|
||||
updated_at: Utils.to_masto_date(marker1.updated_at),
|
||||
version: 0,
|
||||
pleroma: %{unread_count: 5}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
test "renders updated_at in the Mastodon date format" do
|
||||
marker = insert(:marker, timeline: "home")
|
||||
|
||||
%{"home" => %{updated_at: updated_at}} =
|
||||
MarkerView.render("markers.json", %{markers: [marker]})
|
||||
|
||||
assert updated_at =~ ~r/\.\d{3}Z$/
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -345,7 +345,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
|||
bookmark_folder: nil,
|
||||
list_id: nil
|
||||
},
|
||||
quotes_count: 0
|
||||
quotes_count: 0,
|
||||
quote: nil
|
||||
}
|
||||
|
||||
assert status == expected
|
||||
|
|
@ -507,6 +508,63 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
|||
refute status.pleroma.quote_visible
|
||||
end
|
||||
|
||||
describe "Mastodon quote entity" do
|
||||
test "a quote has the quoted status, and quoted statuses only a shallow quote" do
|
||||
post = insert(:note_activity)
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, quote_post} = CommonAPI.post(user, %{status: "he", quoted_status_id: post.id})
|
||||
|
||||
{:ok, quoted_quote_post} =
|
||||
CommonAPI.post(user, %{status: "yo", quoted_status_id: quote_post.id})
|
||||
|
||||
status = StatusView.render("show.json", %{activity: quoted_quote_post})
|
||||
|
||||
assert %{state: "accepted", quoted_status: %{id: quoted_id} = quoted} = status.quote
|
||||
assert quoted_id == to_string(quote_post.id)
|
||||
assert quoted.quote == %{state: "accepted", quoted_status_id: to_string(post.id)}
|
||||
|
||||
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
|
||||
end
|
||||
|
||||
test "a quote of a post the user cannot see is unauthorized" do
|
||||
user = insert(:user)
|
||||
private = insert(:followers_only_note_activity, user: user)
|
||||
|
||||
quote_private =
|
||||
insert(:note_activity,
|
||||
note: insert(:note, data: %{"quoteUrl" => Object.normalize(private).data["id"]})
|
||||
)
|
||||
|
||||
status = StatusView.render("show.json", %{activity: quote_private})
|
||||
assert status.quote == %{state: "unauthorized", quoted_status: nil}
|
||||
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
|
||||
|
||||
follower = insert(:user)
|
||||
CommonAPI.follow(user, follower)
|
||||
|
||||
status = StatusView.render("show.json", %{activity: quote_private, for: follower})
|
||||
assert %{state: "accepted", quoted_status: %{id: id}} = status.quote
|
||||
assert id == to_string(private.id)
|
||||
end
|
||||
|
||||
test "a quote of a missing post is deleted" do
|
||||
quote_missing =
|
||||
insert(:note_activity,
|
||||
note: insert(:note, data: %{"quoteUrl" => "https://example.com/objects/gone"})
|
||||
)
|
||||
|
||||
status = StatusView.render("show.json", %{activity: quote_missing})
|
||||
assert status.quote == %{state: "deleted", quoted_status: nil}
|
||||
end
|
||||
|
||||
test "a post without a quote has no quote" do
|
||||
status = StatusView.render("show.json", %{activity: insert(:note_activity)})
|
||||
assert status.quote == nil
|
||||
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
|
||||
end
|
||||
end
|
||||
|
||||
test "repost of quote post" do
|
||||
post = insert(:note_activity)
|
||||
user = insert(:user)
|
||||
|
|
@ -559,6 +617,46 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
|||
assert mention.url == recipient_ap_id
|
||||
end
|
||||
|
||||
test "inlined images are media proxied" do
|
||||
ConfigMock |> stub_with(Pleroma.Test.StaticConfig)
|
||||
clear_config([:media_proxy, :enabled], true)
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
content_type: "text/html",
|
||||
status: "hii <img src=\"https://example.org/a.png\" />"
|
||||
})
|
||||
|
||||
activity = Repo.get(Activity, activity.id)
|
||||
status = StatusView.render("show.json", activity: activity)
|
||||
|
||||
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
|
||||
|
||||
assert status[:content] =~
|
||||
~r/^hii <img src="https?:\/\/[^\/]+\/proxy\/[^\/]+\/aHR0cHM6Ly9leGFtcGxlLm9yZy9hLnBuZw\/a.png"/
|
||||
end
|
||||
|
||||
test "inlined images using network-path ref are media proxied" do
|
||||
ConfigMock |> stub_with(Pleroma.Test.StaticConfig)
|
||||
clear_config([:media_proxy, :enabled], true)
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
content_type: "text/html",
|
||||
status: "hii <img src=\"//example.org/a.png\" />"
|
||||
})
|
||||
|
||||
activity = Repo.get(Activity, activity.id)
|
||||
status = StatusView.render("show.json", activity: activity)
|
||||
|
||||
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
|
||||
|
||||
assert status[:content] =~
|
||||
~r/^hii <img src="https?:\/\/[^\/]+\/proxy\/[^\/]+\/aHR0cHM6Ly9leGFtcGxlLm9yZy9hLnBuZw\/a.png"/
|
||||
end
|
||||
|
||||
test "create mentions from the 'tag' field" do
|
||||
recipient = insert(:user)
|
||||
cc = insert_pair(:user) |> Enum.map(& &1.ap_id)
|
||||
|
|
@ -597,6 +695,24 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
|||
end
|
||||
|
||||
describe "attachments" do
|
||||
test "skips meta for zero dimensions" do
|
||||
for {width, height} <- [{0, 0}, {200, 0}, {0, 100}] do
|
||||
object = %{
|
||||
"type" => "Image",
|
||||
"url" => [
|
||||
%{
|
||||
"mediaType" => "image/png",
|
||||
"href" => "someurl",
|
||||
"width" => width,
|
||||
"height" => height
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
refute Map.has_key?(StatusView.render("attachment.json", %{attachment: object}), :meta)
|
||||
end
|
||||
end
|
||||
|
||||
test "Complete Mastodon style" do
|
||||
object = %{
|
||||
"type" => "Image",
|
||||
|
|
@ -672,7 +788,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
|||
|
||||
test "put the url advertised in the Activity in to the url attribute" do
|
||||
id = "https://wedistribute.org/wp-json/pterotype/v1/object/85810"
|
||||
[activity] = Activity.search(nil, id)
|
||||
[activity] = Activity.search(insert(:user), id, resolve: true)
|
||||
|
||||
status = StatusView.render("show.json", %{activity: activity})
|
||||
|
||||
|
|
|
|||
|
|
@ -252,22 +252,27 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
|
|||
assert redirected_to(response, 301) == media_proxy_url
|
||||
end
|
||||
|
||||
test "with `static` param and non-GIF image preview requested, " <>
|
||||
"redirects to media preview proxy URI without `static` param",
|
||||
test "with `static` param and non-GIF image preview requested, responds with a preview",
|
||||
%{
|
||||
conn: conn,
|
||||
url: url,
|
||||
media_proxy_url: media_proxy_url
|
||||
} do
|
||||
assert_dependencies_installed()
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{method: :head, url: ^media_proxy_url} ->
|
||||
%Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]}
|
||||
|
||||
%{method: :get, url: ^media_proxy_url} ->
|
||||
%Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
|
||||
end)
|
||||
|
||||
response = get(conn, url <> "?static=true")
|
||||
|
||||
assert response.status == 302
|
||||
assert redirected_to(response) == url
|
||||
assert response.status == 200
|
||||
assert Conn.get_resp_header(response, "content-type") == ["image/jpeg"]
|
||||
assert response.resp_body != ""
|
||||
end
|
||||
|
||||
test "with :min_content_length setting not matched by Content-Length header, " <>
|
||||
|
|
|
|||
|
|
@ -45,6 +45,18 @@ defmodule Pleroma.Web.MediaProxyTest do
|
|||
assert MediaProxy.url(local_root) == local_root
|
||||
end
|
||||
|
||||
test "ignores data url" do
|
||||
assert MediaProxy.url("data:image/png;base64,AAAA") == "data:image/png;base64,AAAA"
|
||||
assert MediaProxy.url("DATA:image/png;base64,AAAA") == "DATA:image/png;base64,AAAA"
|
||||
end
|
||||
|
||||
test "preview_url ignores data url" do
|
||||
clear_config([:media_preview_proxy, :enabled], true)
|
||||
|
||||
assert MediaProxy.preview_url("data:image/png;base64,AAAA") ==
|
||||
"data:image/png;base64,AAAA"
|
||||
end
|
||||
|
||||
test "encodes and decodes URL" do
|
||||
url = "https://pleroma.soykaf.com/static/logo.png"
|
||||
encoded = MediaProxy.url(url)
|
||||
|
|
@ -59,6 +71,20 @@ defmodule Pleroma.Web.MediaProxyTest do
|
|||
assert decode_result(encoded) == url
|
||||
end
|
||||
|
||||
test "encodes and decodes a network-path reference URL as HTTPS" do
|
||||
url = "//example.org/static/logo.png"
|
||||
encoded = MediaProxy.url(url)
|
||||
|
||||
assert String.starts_with?(
|
||||
encoded,
|
||||
Config.get([:media_proxy, :base_url], Pleroma.Web.Endpoint.url())
|
||||
)
|
||||
|
||||
assert String.ends_with?(encoded, "/logo.png")
|
||||
|
||||
assert decode_result(encoded) == "https:" <> url
|
||||
end
|
||||
|
||||
test "encodes and decodes URL without a path" do
|
||||
url = "https://pleroma.soykaf.com"
|
||||
encoded = MediaProxy.url(url)
|
||||
|
|
|
|||
|
|
@ -52,36 +52,60 @@ defmodule Pleroma.Web.OAuth.TokenControllerTest do
|
|||
end
|
||||
|
||||
describe "GET /api/oauth_tokens" do
|
||||
setup do
|
||||
token = insert(:oauth_token) |> Repo.preload(:user)
|
||||
|
||||
%{token: token}
|
||||
end
|
||||
|
||||
test "renders list", %{token: token} do
|
||||
response =
|
||||
build_conn()
|
||||
|> assign(:user, token.user)
|
||||
|> get("/api/oauth_tokens")
|
||||
test "renders list" do
|
||||
%{conn: conn} = oauth_access(["read:security"])
|
||||
|
||||
keys =
|
||||
json_response(response, 200)
|
||||
conn
|
||||
|> get("/api/oauth_tokens")
|
||||
|> json_response(200)
|
||||
|> hd()
|
||||
|> Map.keys()
|
||||
|
||||
assert Enum.sort(keys) == Enum.sort(["id", "app_name", "valid_until", "scopes"])
|
||||
end
|
||||
|
||||
test "revoke token", %{token: token} do
|
||||
response =
|
||||
build_conn()
|
||||
|> assign(:user, token.user)
|
||||
|> delete("/api/oauth_tokens/#{token.id}")
|
||||
test "is allowed with the read scope" do
|
||||
%{conn: conn} = oauth_access(["read"])
|
||||
|
||||
tokens = Token.get_user_tokens(token.user)
|
||||
assert [_] = conn |> get("/api/oauth_tokens") |> json_response(200)
|
||||
end
|
||||
|
||||
assert tokens == []
|
||||
assert response.status == 201
|
||||
test "is forbidden without read:security" do
|
||||
%{conn: conn} = oauth_access(["profile", "read:accounts", "write:security"])
|
||||
|
||||
assert %{"error" => "Insufficient permissions: read:security."} =
|
||||
conn |> get("/api/oauth_tokens") |> json_response(403)
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE /api/oauth_tokens/:id" do
|
||||
test "revokes the token" do
|
||||
%{conn: conn, user: user, token: token} = oauth_access(["write:security"])
|
||||
other = insert(:oauth_token, user: user)
|
||||
|
||||
conn = delete(conn, "/api/oauth_tokens/#{other.id}")
|
||||
|
||||
assert conn.status == 201
|
||||
assert [%Token{id: id}] = Token.get_user_tokens(user)
|
||||
assert id == token.id
|
||||
end
|
||||
|
||||
test "is allowed with the write scope" do
|
||||
%{conn: conn, user: user} = oauth_access(["write"])
|
||||
other = insert(:oauth_token, user: user)
|
||||
|
||||
assert delete(conn, "/api/oauth_tokens/#{other.id}").status == 201
|
||||
end
|
||||
|
||||
test "is forbidden without write:security" do
|
||||
%{conn: conn, user: user} = oauth_access(["profile", "read", "write:accounts"])
|
||||
other = insert(:oauth_token, user: user)
|
||||
|
||||
assert %{"error" => "Insufficient permissions: write:security."} =
|
||||
conn |> delete("/api/oauth_tokens/#{other.id}") |> json_response(403)
|
||||
|
||||
assert length(Token.get_user_tokens(user)) == 2
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -477,4 +477,53 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
|
|||
|> get("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐈")
|
||||
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
|
||||
end
|
||||
|
||||
describe "rate limiting" do
|
||||
test "PUT and DELETE share a per-status limit", %{conn: conn} do
|
||||
clear_config([:rate_limit, :status_id_action], {60_000, 2})
|
||||
user = insert(:user)
|
||||
%{id: activity_id} = insert(:note_activity)
|
||||
%{id: other_activity_id} = insert(:note_activity)
|
||||
conn = prepare_conn_of_user(conn, user)
|
||||
|
||||
conn
|
||||
|> put("/api/v1/pleroma/statuses/#{activity_id}/reactions/☕")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
conn
|
||||
|> delete("/api/v1/pleroma/statuses/#{activity_id}/reactions/☕")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert conn
|
||||
|> put("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐈")
|
||||
|> json_response(429) == %{"error" => "Throttled"}
|
||||
|
||||
conn
|
||||
|> put("/api/v1/pleroma/statuses/#{other_activity_id}/reactions/🐈")
|
||||
|> json_response_and_validate_schema(200)
|
||||
end
|
||||
|
||||
test "PUT and DELETE count against statuses_actions", %{conn: conn} do
|
||||
clear_config([:rate_limit, :statuses_actions], {60_000, 2})
|
||||
user = insert(:user)
|
||||
[first, second, third] = insert_list(3, :note_activity)
|
||||
conn = prepare_conn_of_user(conn, user)
|
||||
|
||||
conn
|
||||
|> put("/api/v1/pleroma/statuses/#{first.id}/reactions/☕")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
conn
|
||||
|> delete("/api/v1/pleroma/statuses/#{first.id}/reactions/☕")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert conn
|
||||
|> put("/api/v1/pleroma/statuses/#{second.id}/reactions/☕")
|
||||
|> json_response(429) == %{"error" => "Throttled"}
|
||||
|
||||
assert conn
|
||||
|> delete("/api/v1/pleroma/statuses/#{third.id}/reactions/☕")
|
||||
|> json_response(429) == %{"error" => "Throttled"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
42
test/pleroma/web/plugs/http_signature_plug_mrf_test.exs
Normal file
42
test/pleroma/web/plugs/http_signature_plug_mrf_test.exs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.HTTPSignaturePlugMRFTest do
|
||||
# Not async: the MRF settings are global config.
|
||||
use Pleroma.Web.ConnCase
|
||||
|
||||
alias Pleroma.StaticStubbedConfigMock, as: ConfigMock
|
||||
alias Pleroma.StubbedHTTPSignaturesMock, as: HTTPSignaturesMock
|
||||
alias Pleroma.Web.Plugs.HTTPSignaturePlug
|
||||
|
||||
import Mox
|
||||
import Phoenix.Controller, only: [put_format: 2]
|
||||
import Plug.Conn
|
||||
|
||||
setup do: clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
|
||||
setup do: clear_config([:mrf_simple, :reject], [{"mastodon.example.org", "no reason"}])
|
||||
|
||||
defp signed_get(actor) do
|
||||
ConfigMock
|
||||
|> expect(:get, fn [:activitypub, :authorized_fetch_mode], false -> true end)
|
||||
|> expect(:get, fn [:instance, :rejected_instances] -> [] end)
|
||||
|
||||
HTTPSignaturesMock
|
||||
|> expect(:validate_conn, fn _ -> true end)
|
||||
|
||||
build_conn(:get, "/doesntmattter", %{"actor" => actor})
|
||||
|> put_req_header("date", Pleroma.Signature.signed_date())
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"#{actor}#main-key\",headers=\"(request-target) host date\",signature=\"fake\""
|
||||
)
|
||||
|> put_format("activity+json")
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
end
|
||||
|
||||
test "rejects signed requests from instances rejected by SimplePolicy" do
|
||||
assert signed_get("http://mastodon.example.org/users/admin").halted
|
||||
refute signed_get("http://allowed.example.org/users/admin").halted
|
||||
end
|
||||
end
|
||||
119
test/pleroma/web/plugs/http_signature_plug_policy_test.exs
Normal file
119
test/pleroma/web/plugs/http_signature_plug_policy_test.exs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.HTTPSignaturePlugPolicyTest do
|
||||
# Not async: changes global config and uses global HTTP mocks.
|
||||
use Pleroma.Web.ConnCase
|
||||
|
||||
alias Pleroma.Keys
|
||||
alias Pleroma.Object.Fetcher
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.StubbedHTTPSignaturesMock, as: HTTPSignaturesMock
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.InternalFetchActor
|
||||
alias Pleroma.Web.ActivityPub.Publisher
|
||||
alias Pleroma.Web.Plugs.DigestPlug
|
||||
alias Pleroma.Web.Plugs.HTTPSignaturePlug
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
import Mox
|
||||
import Phoenix.Controller, only: [put_format: 2]
|
||||
import Pleroma.Factory
|
||||
import Plug.Conn
|
||||
|
||||
defp incoming_conn(method, url, headers, body \\ "") do
|
||||
%URI{host: host, path: path} = URI.parse(url)
|
||||
|
||||
headers = Enum.map(headers, fn {name, value} -> {String.downcase(name), value} end)
|
||||
|
||||
Plug.Test.conn(method, path, body)
|
||||
|> Map.put(:req_headers, [{"host", host} | headers])
|
||||
|> put_format("activity+json")
|
||||
end
|
||||
|
||||
# Local users have no public_key column set; publish it like a remote copy would.
|
||||
defp store_public_key(%User{} = user) do
|
||||
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
|
||||
entry = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
|
||||
|
||||
user
|
||||
|> Ecto.Changeset.change(public_key: :public_key.pem_encode([entry]))
|
||||
|> Repo.update!()
|
||||
|> User.set_cache()
|
||||
end
|
||||
|
||||
defp capture_request(method) do
|
||||
test_pid = self()
|
||||
|
||||
Tesla.Mock.mock_global(fn %{method: ^method, headers: headers, body: body} = env ->
|
||||
send(test_pid, {:request, headers, body})
|
||||
%{env | status: 200, body: ""}
|
||||
end)
|
||||
end
|
||||
|
||||
test "accepts deliveries signed by Pleroma's publisher" do
|
||||
stub_with(Pleroma.Web.ActivityPub.TransmogrifierMock, Pleroma.Web.ActivityPub.Transmogrifier)
|
||||
capture_request(:post)
|
||||
|
||||
activity = insert(:note_activity)
|
||||
store_public_key(User.get_by_ap_id(activity.data["actor"]))
|
||||
inbox = "https://remote.example/inbox"
|
||||
|
||||
{:ok, _} =
|
||||
%{inbox: inbox, activity_id: activity.id}
|
||||
|> Publisher.prepare_one()
|
||||
|> Publisher.publish_one()
|
||||
|
||||
assert_received {:request, headers, body}
|
||||
|
||||
{:ok, _, conn} =
|
||||
incoming_conn(:post, inbox, [{"content-length", "#{byte_size(body)}"} | headers], body)
|
||||
|> DigestPlug.read_body([])
|
||||
|
||||
conn = HTTPSignaturePlug.call(conn, %{})
|
||||
|
||||
assert conn.assigns.valid_signature == true
|
||||
refute conn.halted
|
||||
end
|
||||
|
||||
test "accepts object fetches signed by Pleroma's fetcher" do
|
||||
clear_config([:activitypub, :sign_object_fetches], true)
|
||||
capture_request(:get)
|
||||
store_public_key(InternalFetchActor.get_actor())
|
||||
|
||||
url = "https://remote.example/objects/1"
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(url)
|
||||
|
||||
assert_received {:request, headers, _body}
|
||||
|
||||
conn =
|
||||
incoming_conn(:get, url, headers)
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
|
||||
assert conn.assigns.valid_signature == true
|
||||
end
|
||||
|
||||
test "only logs policy violations when the policy is set to :log" do
|
||||
clear_config([:activitypub, :signature_policy], :log)
|
||||
|
||||
HTTPSignaturesMock
|
||||
|> expect(:validate_conn, fn _ -> true end)
|
||||
|
||||
log =
|
||||
capture_log(fn ->
|
||||
conn =
|
||||
incoming_conn(:post, "https://pleroma.example/inbox", [
|
||||
{"date", Pleroma.Signature.signed_date()},
|
||||
{"signature",
|
||||
"keyId=\"https://remote.example/users/a#main-key\",headers=\"(request-target) host date\",signature=\"fake\""}
|
||||
])
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
|
||||
assert conn.assigns.valid_signature == true
|
||||
refute conn.halted
|
||||
end)
|
||||
|
||||
assert log =~ "digest_not_signed"
|
||||
end
|
||||
end
|
||||
|
|
@ -13,6 +13,15 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
|
|||
import Phoenix.Controller, only: [put_format: 2]
|
||||
import Plug.Conn
|
||||
|
||||
defp put_signature(conn, actor, signed_headers \\ "(request-target) host date") do
|
||||
conn
|
||||
|> put_req_header("date", Pleroma.Signature.signed_date())
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"#{actor}#main-key\",headers=\"#{signed_headers}\",signature=\"fake\""
|
||||
)
|
||||
end
|
||||
|
||||
test "it calls HTTPSignatures to check validity if the actor signed it" do
|
||||
params = %{"actor" => "http://mastodon.example.org/users/admin"}
|
||||
conn = build_conn(:get, "/doesntmattter", params)
|
||||
|
|
@ -22,10 +31,7 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
|
|||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"http://mastodon.example.org/users/admin#main-key"
|
||||
)
|
||||
|> put_signature("http://mastodon.example.org/users/admin")
|
||||
|> put_format("activity+json")
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
|
||||
|
|
@ -51,10 +57,7 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
|
|||
|
||||
conn =
|
||||
orig_conn
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"http://mastodon.example.org/users/admin#main-key"
|
||||
)
|
||||
|> put_signature("http://mastodon.example.org/users/admin")
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
|
||||
assert conn.assigns.valid_signature == false
|
||||
|
|
@ -71,10 +74,7 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
|
|||
|
||||
conn =
|
||||
orig_conn
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"http://mastodon.example.org/users/admin#main-key"
|
||||
)
|
||||
|> put_signature("http://mastodon.example.org/users/admin")
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
|
||||
assert conn.assigns.valid_signature == true
|
||||
|
|
@ -108,10 +108,7 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
|
|||
conn =
|
||||
conn
|
||||
|> Map.put(:remote_ip, {192, 168, 0, 1})
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"http://mastodon.example.org/users/admin#main-key"
|
||||
)
|
||||
|> put_signature("http://mastodon.example.org/users/admin")
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
|
||||
assert conn.remote_ip == {192, 168, 0, 1}
|
||||
|
|
@ -131,10 +128,7 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
|
|||
|
||||
conn =
|
||||
build_conn(:get, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"})
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"http://mastodon.example.org/users/admin#main-key"
|
||||
)
|
||||
|> put_signature("http://mastodon.example.org/users/admin")
|
||||
|> put_format("activity+json")
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
|
||||
|
|
@ -152,14 +146,84 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
|
|||
|
||||
conn =
|
||||
build_conn(:get, "/doesntmattter", %{"actor" => "http://allowed.example.org/users/admin"})
|
||||
|> put_req_header(
|
||||
"signature",
|
||||
"keyId=\"http://allowed.example.org/users/admin#main-key"
|
||||
)
|
||||
|> put_signature("http://allowed.example.org/users/admin")
|
||||
|> put_format("activity+json")
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
|
||||
assert conn.assigns.valid_signature == true
|
||||
assert conn.halted == false
|
||||
end
|
||||
|
||||
describe "signature policy" do
|
||||
test "treats a signed GET with a stale date as unsigned" do
|
||||
HTTPSignaturesMock
|
||||
|> expect(:validate_conn, 0, fn _ -> true end)
|
||||
|
||||
stale = NaiveDateTime.add(NaiveDateTime.utc_now(), -3 * 3600)
|
||||
|
||||
conn =
|
||||
build_conn(:get, "/doesntmattter")
|
||||
|> put_signature("http://mastodon.example.org/users/admin")
|
||||
|> put_req_header("date", Pleroma.Signature.signed_date(stale))
|
||||
|> put_format("activity+json")
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
|
||||
assert conn.assigns.valid_signature == false
|
||||
assert conn.assigns.signature_error == :date_too_old
|
||||
refute conn.halted
|
||||
end
|
||||
|
||||
test "marks a signed POST that does not cover the digest as invalid" do
|
||||
HTTPSignaturesMock
|
||||
|> expect(:validate_conn, 0, fn _ -> true end)
|
||||
|
||||
conn =
|
||||
build_conn(:post, "/inbox")
|
||||
|> put_signature("http://mastodon.example.org/users/admin")
|
||||
|> put_format("activity+json")
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
|
||||
assert conn.assigns.valid_signature == false
|
||||
assert conn.assigns.signature_error == :digest_not_signed
|
||||
refute conn.halted
|
||||
end
|
||||
|
||||
test "uses the digest computed from the body for signed POSTs" do
|
||||
HTTPSignaturesMock
|
||||
|> expect(:validate_conn, fn conn ->
|
||||
get_req_header(conn, "digest") == ["SHA-256=computed"]
|
||||
end)
|
||||
|
||||
conn =
|
||||
build_conn(:post, "/inbox")
|
||||
|> put_signature(
|
||||
"http://mastodon.example.org/users/admin",
|
||||
"(request-target) host date digest"
|
||||
)
|
||||
|> put_req_header("digest", "SHA-256=claimed")
|
||||
|> assign(:digest, "SHA-256=computed")
|
||||
|> put_format("activity+json")
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
|
||||
assert conn.assigns.valid_signature == true
|
||||
end
|
||||
|
||||
test "marks a signed POST whose body was not hashed as invalid" do
|
||||
HTTPSignaturesMock
|
||||
|> expect(:validate_conn, 0, fn _ -> true end)
|
||||
|
||||
conn =
|
||||
build_conn(:post, "/inbox")
|
||||
|> put_signature(
|
||||
"http://mastodon.example.org/users/admin",
|
||||
"(request-target) host date digest"
|
||||
)
|
||||
|> put_req_header("digest", "SHA-256=claimed")
|
||||
|> put_format("activity+json")
|
||||
|> HTTPSignaturePlug.call(%{})
|
||||
|
||||
assert conn.assigns.valid_signature == false
|
||||
assert conn.assigns.signature_error == :body_not_hashed
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -50,7 +50,9 @@ defmodule Pleroma.Web.Plugs.RateLimiterTest do
|
|||
|
||||
test "it restricts based on config values" do
|
||||
limiter_name = :test_plug_opts
|
||||
scale = 80
|
||||
# Long enough that the bucket cannot expire during the test on slow CI.
|
||||
# The test expires it explicitly with expire_ttl/2.
|
||||
scale = 60_000
|
||||
limit = 5
|
||||
|
||||
clear_config([Pleroma.Web.Endpoint, :http, :ip], {127, 0, 0, 1})
|
||||
|
|
@ -184,7 +186,8 @@ defmodule Pleroma.Web.Plugs.RateLimiterTest do
|
|||
test "can have limits separate from unauthenticated connections" do
|
||||
limiter_name = :test_authenticated1
|
||||
|
||||
scale = 50
|
||||
# Long enough that the bucket cannot expire during the test on slow CI.
|
||||
scale = 60_000
|
||||
limit = 5
|
||||
clear_config([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8})
|
||||
clear_config([:rate_limit, limiter_name], [{1000, 1}, {scale, limit}])
|
||||
|
|
|
|||
25
test/pleroma/web/user_socket_test.exs
Normal file
25
test/pleroma/web/user_socket_test.exs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.UserSocketTest do
|
||||
use Pleroma.Web.ChannelCase
|
||||
alias Pleroma.Web.UserSocket
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
test "it connects with a valid token" do
|
||||
user = insert(:user)
|
||||
token = Phoenix.Token.sign(Pleroma.Web.Endpoint, "user socket", user.id)
|
||||
|
||||
assert {:ok, socket} = connect(UserSocket, %{"token" => token})
|
||||
assert socket.assigns.user_name == user.nickname
|
||||
end
|
||||
|
||||
# A crash here would put the secret key base into the error log.
|
||||
test "it rejects tokens that are not strings" do
|
||||
for token <- [["x"], 123, %{"a" => "b"}] do
|
||||
assert :error = connect(UserSocket, %{"token" => token})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -305,4 +305,30 @@ defmodule Pleroma.Web.WebFingerTest do
|
|||
{:ok, _data} = WebFinger.finger("a@sub.mastodon.example")
|
||||
end
|
||||
end
|
||||
|
||||
describe "rate limiting" do
|
||||
setup do
|
||||
# the module setup stubs NullCache, the backoff needs a real cache
|
||||
Mox.stub_with(Pleroma.CachexMock, Pleroma.CachexProxy)
|
||||
test_pid = self()
|
||||
|
||||
Tesla.Mock.mock_global(fn %{url: url} ->
|
||||
send(test_pid, {:request, url})
|
||||
%Tesla.Env{status: 429, headers: [{"retry-after", "600"}]}
|
||||
end)
|
||||
end
|
||||
|
||||
test "backs off from a host that rate limits host-meta" do
|
||||
assert {:error, :ratelimit} = WebFinger.finger("alice@ratelimited.example")
|
||||
assert_received {:request, "https://ratelimited.example/.well-known/host-meta"}
|
||||
refute_received {:request, _}
|
||||
|
||||
# a rate-limited host-meta lookup must not be cached as "no LRDD template"
|
||||
assert {:ok, nil} = Cachex.get(:host_meta_cache, "ratelimited.example")
|
||||
|
||||
assert {:error, :ratelimit} = WebFinger.finger("bob@ratelimited.example")
|
||||
assert {:error, :ratelimit} = WebFinger.find_lrdd_template("ratelimited.example")
|
||||
refute_received {:request, _}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -153,4 +153,38 @@ defmodule Pleroma.Workers.PublisherWorkerTest do
|
|||
assert {:cancel, :unreachable} = PublisherWorker.perform(job)
|
||||
end
|
||||
end
|
||||
|
||||
describe "Blocked instances:" do
|
||||
defp publish_one_job(inbox, activity) do
|
||||
%Oban.Job{
|
||||
args: %{
|
||||
"op" => "publish_one",
|
||||
"params" => %{"inbox" => inbox, "activity_id" => activity.id}
|
||||
},
|
||||
attempt: 1,
|
||||
max_attempts: 5
|
||||
}
|
||||
end
|
||||
|
||||
test "cancels queued jobs to instances rejected by SimplePolicy" do
|
||||
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
|
||||
clear_config([:mrf_simple, :reject], [{"rejected.example", ""}])
|
||||
{:ok, activity} = CommonAPI.post(insert(:user), %{status: "public"})
|
||||
|
||||
assert {:cancel, :rejected} =
|
||||
PublisherWorker.perform(
|
||||
publish_one_job("https://rejected.example/inbox", activity)
|
||||
)
|
||||
end
|
||||
|
||||
test "cancels queued private jobs to quarantined instances" do
|
||||
clear_config([:instance, :quarantined_instances], [{"quarantined.example", ""}])
|
||||
{:ok, activity} = CommonAPI.post(insert(:user), %{status: "hi", visibility: "private"})
|
||||
|
||||
assert {:cancel, :rejected} =
|
||||
PublisherWorker.perform(
|
||||
publish_one_job("https://quarantined.example/inbox", activity)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -133,6 +133,28 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "rate-limited fetches" do
|
||||
setup do
|
||||
Cachex.put(:http_backoff_cache, "ratelimited.example", true)
|
||||
|
||||
params =
|
||||
insert(:note_activity).data
|
||||
|> Map.put("actor", "https://ratelimited.example/users/alice")
|
||||
|
||||
%{args: %{"op" => "incoming_ap_doc", "params" => params}}
|
||||
end
|
||||
|
||||
test "snooze while the actor's host is backed off", %{args: args} do
|
||||
assert {:snooze, 300} = perform_job(ReceiverWorker, args)
|
||||
end
|
||||
|
||||
test "stop snoozing two hours after the job was inserted", %{args: args} do
|
||||
inserted_at = DateTime.add(DateTime.utc_now(), -121, :minute)
|
||||
|
||||
assert {:error, :ratelimit} = perform_job(ReceiverWorker, args, inserted_at: inserted_at)
|
||||
end
|
||||
end
|
||||
|
||||
test "cancels due to origin containment" do
|
||||
params =
|
||||
insert(:note_activity).data
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ defmodule Pleroma.Workers.RemoteFetcherWorkerTest do
|
|||
@unauthorized_object "https://unauthorized.example.com/"
|
||||
@depth_object "https://depth.example.com/"
|
||||
@content_type_object "https://bad_content_type.example.com/"
|
||||
@ratelimited_object "https://ratelimited.example.com/"
|
||||
|
||||
describe "RemoteFetcherWorker" do
|
||||
setup do
|
||||
|
|
@ -37,6 +38,11 @@ defmodule Pleroma.Workers.RemoteFetcherWorkerTest do
|
|||
status: 200
|
||||
}
|
||||
|
||||
%{method: :get, url: @ratelimited_object} ->
|
||||
%Tesla.Env{
|
||||
status: 429
|
||||
}
|
||||
|
||||
%{method: :get, url: @content_type_object} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
|
|
@ -73,6 +79,14 @@ defmodule Pleroma.Workers.RemoteFetcherWorkerTest do
|
|||
})
|
||||
end
|
||||
|
||||
test "retries jobs for a rate-limited host" do
|
||||
assert {:error, :ratelimit} =
|
||||
perform_job(RemoteFetcherWorker, %{
|
||||
"op" => "fetch_remote",
|
||||
"id" => @ratelimited_object
|
||||
})
|
||||
end
|
||||
|
||||
test "does not retry jobs for when object returns wrong content type" do
|
||||
assert {:cancel, _} =
|
||||
perform_job(RemoteFetcherWorker, %{
|
||||
|
|
|
|||
|
|
@ -306,6 +306,24 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do
|
|||
refute Activity.get_by_ap_id(create["id"])
|
||||
end
|
||||
|
||||
test "snoozes while the payload actor's host is backed off" do
|
||||
Cachex.put(:http_backoff_cache, "ratelimited.example", true)
|
||||
alice = build(:user, local: false, ap_id: "https://ratelimited.example/users/alice")
|
||||
|
||||
params = %{
|
||||
"type" => "Create",
|
||||
"actor" => alice.ap_id,
|
||||
"id" => "https://ratelimited.example/activities/1",
|
||||
"object" => "https://ratelimited.example/objects/1"
|
||||
}
|
||||
|
||||
job = failed_signature_job(params, signature_headers_for(alice))
|
||||
assert {:snooze, 300} = SignatureRetryWorker.perform(job)
|
||||
|
||||
old_job = %{job | inserted_at: DateTime.add(DateTime.utc_now(), -121, :minute)}
|
||||
assert {:error, :ratelimit} = SignatureRetryWorker.perform(old_job)
|
||||
end
|
||||
|
||||
test "processes the activity after refetching a valid matching signature" do
|
||||
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
|
||||
|
||||
|
|
|
|||
44
test/support/exif_helpers.ex
Normal file
44
test/support/exif_helpers.ex
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Tests.ExifHelpers do
|
||||
@moduledoc """
|
||||
Helpers for upload filter tests that use exiftool as an independent oracle.
|
||||
"""
|
||||
|
||||
@doc "Copies a fixture to a unique temporary file and returns an upload for it."
|
||||
def upload_copy(fixture, content_type, attrs \\ []) do
|
||||
source = Path.absname(Path.join("test/fixtures", fixture))
|
||||
|
||||
tempfile =
|
||||
Path.join(
|
||||
System.tmp_dir!(),
|
||||
"pleroma_exif_#{System.unique_integer([:positive])}#{Path.extname(fixture)}"
|
||||
)
|
||||
|
||||
File.cp!(source, tempfile)
|
||||
ExUnit.Callbacks.on_exit(fn -> File.rm(tempfile) end)
|
||||
|
||||
struct!(
|
||||
%Pleroma.Upload{
|
||||
name: fixture,
|
||||
content_type: content_type,
|
||||
path: source,
|
||||
tempfile: tempfile
|
||||
},
|
||||
attrs
|
||||
)
|
||||
end
|
||||
|
||||
@doc "Returns the GPS tags exiftool finds in a file, as text."
|
||||
def gps_tags(path), do: exiftool!(["-gps:all", path])
|
||||
|
||||
@doc "Returns the value of one tag as exiftool reads it."
|
||||
def tag(path, tag), do: exiftool!(["-s3", "-#{tag}", path])
|
||||
|
||||
defp exiftool!(args) do
|
||||
{output, 0} = System.cmd("exiftool", ["-m" | args])
|
||||
String.trim(output)
|
||||
end
|
||||
end
|
||||
|
|
@ -577,6 +577,16 @@ defmodule HttpRequestMock do
|
|||
}}
|
||||
end
|
||||
|
||||
# the same status via its canonical id
|
||||
def get("http://mastodon.example.org/users/admin/statuses/99541947525187367", _, _, _) do
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: File.read!("test/fixtures/mastodon-note-object.json"),
|
||||
headers: activitypub_object_headers()
|
||||
}}
|
||||
end
|
||||
|
||||
def get("http://mastodon.example.org/@admin/99541947525187368", _, _, _) do
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue