Merge remote-tracking branch 'origin/develop' into pr-7444-finish
This commit is contained in:
commit
7a741736ea
1544 changed files with 43219 additions and 9351 deletions
|
|
@ -249,7 +249,7 @@ defmodule Pleroma.ActivityTest do
|
|||
{:ok, %{id: id, object: %{data: %{"id" => obj_id}}}} =
|
||||
Pleroma.Web.CommonAPI.post(user, %{status: "cofe"})
|
||||
|
||||
Pleroma.Web.CommonAPI.favorite(another, id)
|
||||
Pleroma.Web.CommonAPI.favorite(id, another)
|
||||
|
||||
assert obj_id
|
||||
|> Pleroma.Activity.Queries.by_object_id()
|
||||
|
|
@ -261,23 +261,27 @@ defmodule Pleroma.ActivityTest do
|
|||
test "add_by_params_query/3" do
|
||||
user = insert(:user)
|
||||
|
||||
note = insert(:note_activity, user: user)
|
||||
note_activity = insert(:note_activity, user: user)
|
||||
|
||||
insert(:add_activity, user: user, note: note)
|
||||
insert(:add_activity, user: user, note: note)
|
||||
insert(:add_activity, user: user, note_activity: note_activity)
|
||||
insert(:add_activity, user: user, note_activity: note_activity)
|
||||
insert(:add_activity, user: user)
|
||||
|
||||
assert Repo.aggregate(Activity, :count, :id) == 4
|
||||
assert Repo.aggregate(Activity, :count, :id) == 5
|
||||
|
||||
add_query =
|
||||
Activity.add_by_params_query(note.data["object"], user.ap_id, user.featured_address)
|
||||
Activity.add_by_params_query(
|
||||
note_activity.data["object"],
|
||||
user.ap_id,
|
||||
user.featured_address
|
||||
)
|
||||
|
||||
assert Repo.aggregate(add_query, :count, :id) == 2
|
||||
|
||||
Repo.delete_all(add_query)
|
||||
assert Repo.aggregate(add_query, :count, :id) == 0
|
||||
|
||||
assert Repo.aggregate(Activity, :count, :id) == 2
|
||||
assert Repo.aggregate(Activity, :count, :id) == 3
|
||||
end
|
||||
|
||||
describe "associated_object_id() sql function" do
|
||||
|
|
|
|||
39
test/pleroma/application_test.exs
Normal file
39
test/pleroma/application_test.exs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.ApplicationTest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
|
||||
# TODO HACK: Temporarily disabled.
|
||||
# Currently causes a bunch of warnings about :warn being deprecated in logs.
|
||||
# ex_syslogger needs to either be updated upstream or forked.
|
||||
describe "config :pleroma, :logger, backends: [{:ex_syslogger, :ex_syslogger}]" do
|
||||
setup do
|
||||
clear_config([:logger], backends: [{:ex_syslogger, :ex_syslogger}])
|
||||
end
|
||||
|
||||
@tag :skip
|
||||
test "is handled" do
|
||||
log = capture_log(fn -> Pleroma.Application.configure_logger() end)
|
||||
|
||||
assert log =~
|
||||
"Configuration {:ex_syslogger, :ex_syslogger} is incorrect. Use {ExSyslogger, :ex_syslogger} instead!"
|
||||
|
||||
assert log =~ "Successfully added logger backend: {ExSyslogger, :ex_syslogger}"
|
||||
end
|
||||
end
|
||||
|
||||
describe "config :pleroma, :logger, :backends: [:console]" do
|
||||
setup do
|
||||
clear_config([:logger], backends: [:console])
|
||||
end
|
||||
|
||||
test "emits a warning" do
|
||||
assert capture_log(fn -> Pleroma.Application.configure_logger() end) =~
|
||||
":console is no longer considered a backend and is enabled by default"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Config.DeprecationWarningsTest do
|
||||
use ExUnit.Case
|
||||
use ExUnit.Case, async: false
|
||||
use Pleroma.Tests.Helpers
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
|
|
@ -388,4 +388,62 @@ defmodule Pleroma.Config.DeprecationWarningsTest do
|
|||
end) =~
|
||||
"Your config is using the old namespace for the Shoutbox configuration."
|
||||
end
|
||||
|
||||
describe "check_deprecated_logger_config" do
|
||||
setup do
|
||||
initial_console = Application.get_env(:logger, :console, nil)
|
||||
initial_backends = Application.get_env(:logger, :backends, nil)
|
||||
|
||||
# NOTE: The Logger does not get reconfigured when running in the test env,
|
||||
# the added and on_exit removed settings below should have no effect besides
|
||||
# triggering the deprecation warnings
|
||||
Application.put_env(:logger, :console, level: :all)
|
||||
Application.put_env(:logger, :backends, [:console])
|
||||
|
||||
on_exit(fn ->
|
||||
if initial_console do
|
||||
Application.put_env(:logger, :console, initial_console)
|
||||
else
|
||||
Application.delete_env(:logger, :console)
|
||||
end
|
||||
|
||||
if initial_backends do
|
||||
Application.put_env(:logger, :backends, initial_backends)
|
||||
else
|
||||
Application.delete_env(:logger, :backends)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
test "warns on deprecated syntax" do
|
||||
log =
|
||||
capture_log(fn ->
|
||||
Pleroma.Config.DeprecationWarnings.check_deprecated_logger_config()
|
||||
end)
|
||||
|
||||
assert log =~
|
||||
"""
|
||||
!!!DEPRECATION WARNING!!!
|
||||
Your configuration is using deprecated syntax for configuring backends of Elixir's logger.
|
||||
`config :logger, backends: [...]` is deprecated syntax due to changes in Elixir.
|
||||
Please update your configuration at your earliest convenience to use:
|
||||
`config :pleroma, :logger, backends: [...]`
|
||||
|
||||
Note: `:console` is no longer considered a backend and is used by default, you can disable it using:
|
||||
`config :logger, :default_handler: false`
|
||||
"""
|
||||
|
||||
assert log =~
|
||||
"""
|
||||
!!!DEPRECATION WARNING!!!
|
||||
Your configuration is using deprecated syntax for configuring logging to console.
|
||||
`config :logger, :console` is deprecated syntax due to changes in Elixir.
|
||||
Please update your configuration at your earliest convenience to use
|
||||
`config :logger, default_handler` and `config :logger, :default_formatter`.
|
||||
|
||||
Note: `:default_handler` is used only for the `level` setting. All other configurations go under
|
||||
`:default_formatter`. For more info visit: https://hexdocs.pm/logger/Logger.html#module-backends-and-backwards-compatibility
|
||||
"""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -174,6 +174,22 @@ defmodule Pleroma.ConfigDBTest do
|
|||
assert updated1.value == [groups: [c: 3, d: 4], key: [a: 1, b: 2]]
|
||||
assert updated2.value == [mascots: [c: 3, d: 4], key: [a: 1, b: 2]]
|
||||
end
|
||||
|
||||
test "rejects invalid :rate_limit values (e.g. empty-string scale from AdminFE)" do
|
||||
assert {:error, _changeset} =
|
||||
ConfigDB.update_or_create(%{
|
||||
group: ":pleroma",
|
||||
key: ":rate_limit",
|
||||
value: [
|
||||
%{
|
||||
"tuple" => [
|
||||
":statuses_actions",
|
||||
[%{"tuple" => ["", 0]}, %{"tuple" => ["", ""]}]
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete/1" do
|
||||
|
|
@ -273,24 +289,28 @@ defmodule Pleroma.ConfigDBTest do
|
|||
end
|
||||
|
||||
test "sigil" do
|
||||
assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]") == ~r/comp[lL][aA][iI][nN]er/
|
||||
assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]").source ==
|
||||
~r/comp[lL][aA][iI][nN]er/.source
|
||||
end
|
||||
|
||||
test "link sigil" do
|
||||
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/") == ~r/https:\/\/example.com/
|
||||
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/").source ==
|
||||
~r/https:\/\/example.com/.source
|
||||
end
|
||||
|
||||
test "link sigil with um modifiers" do
|
||||
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/um") ==
|
||||
~r/https:\/\/example.com/um
|
||||
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/um").source ==
|
||||
~r/https:\/\/example.com/um.source
|
||||
end
|
||||
|
||||
test "link sigil with i modifier" do
|
||||
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i") == ~r/https:\/\/example.com/i
|
||||
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i").source ==
|
||||
~r/https:\/\/example.com/i.source
|
||||
end
|
||||
|
||||
test "link sigil with s modifier" do
|
||||
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s") == ~r/https:\/\/example.com/s
|
||||
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s").source ==
|
||||
~r/https:\/\/example.com/s.source
|
||||
end
|
||||
|
||||
test "raise if valid delimiter not found" do
|
||||
|
|
@ -312,7 +332,7 @@ defmodule Pleroma.ConfigDBTest do
|
|||
test "proxy tuple with domain" do
|
||||
assert ConfigDB.to_elixir_types(%{
|
||||
"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]
|
||||
}) == {:proxy_url, {:socks5, 'domain.com', 1234}}
|
||||
}) == {:proxy_url, {:socks5, ~c"domain.com", 1234}}
|
||||
end
|
||||
|
||||
test "proxy tuple with ip" do
|
||||
|
|
@ -460,11 +480,11 @@ defmodule Pleroma.ConfigDBTest do
|
|||
test "complex keyword with sigil" do
|
||||
assert ConfigDB.to_elixir_types([
|
||||
%{"tuple" => [":federated_timeline_removal", []]},
|
||||
%{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]},
|
||||
%{"tuple" => [":reject", [~r/comp[lL][aA][iI][nN]er/.source]]},
|
||||
%{"tuple" => [":replace", []]}
|
||||
]) == [
|
||||
federated_timeline_removal: [],
|
||||
reject: [~r/comp[lL][aA][iI][nN]er/],
|
||||
reject: [~r/comp[lL][aA][iI][nN]er/.source],
|
||||
replace: []
|
||||
]
|
||||
end
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ defmodule Pleroma.Conversation.ParticipationTest do
|
|||
# When it's a reply from the blocked user
|
||||
{:ok, _direct2} =
|
||||
CommonAPI.post(blocked, %{
|
||||
status: "reply",
|
||||
status: "@#{third_user.nickname}, #{blocker.nickname} reply",
|
||||
visibility: "direct",
|
||||
in_reply_to_conversation_id: blocked_participation.id
|
||||
})
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ defmodule Pleroma.ConversationTest do
|
|||
setup_all do: clear_config([:instance, :federating], true)
|
||||
|
||||
setup do
|
||||
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
|
||||
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
|
||||
:ok
|
||||
end
|
||||
|
||||
|
|
@ -66,8 +66,10 @@ defmodule Pleroma.ConversationTest do
|
|||
jafnhar = insert(:user, local: false)
|
||||
tridi = insert(:user)
|
||||
|
||||
to = [har.nickname, jafnhar.nickname, tridi.nickname]
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(har, %{status: "Hey @#{jafnhar.nickname}", visibility: "direct"})
|
||||
CommonAPI.post(har, %{status: "Hey @#{jafnhar.nickname}", visibility: "direct", to: to})
|
||||
|
||||
object = Pleroma.Object.normalize(activity, fetch: false)
|
||||
context = object.data["context"]
|
||||
|
|
@ -88,7 +90,8 @@ defmodule Pleroma.ConversationTest do
|
|||
CommonAPI.post(jafnhar, %{
|
||||
status: "Hey @#{har.nickname}",
|
||||
visibility: "direct",
|
||||
in_reply_to_status_id: activity.id
|
||||
in_reply_to_status_id: activity.id,
|
||||
to: to
|
||||
})
|
||||
|
||||
object = Pleroma.Object.normalize(activity, fetch: false)
|
||||
|
|
@ -112,7 +115,8 @@ defmodule Pleroma.ConversationTest do
|
|||
CommonAPI.post(tridi, %{
|
||||
status: "Hey @#{har.nickname}",
|
||||
visibility: "direct",
|
||||
in_reply_to_status_id: activity.id
|
||||
in_reply_to_status_id: activity.id,
|
||||
to: to
|
||||
})
|
||||
|
||||
object = Pleroma.Object.normalize(activity, fetch: false)
|
||||
|
|
|
|||
16
test/pleroma/dependency_version_test.exs
Normal file
16
test/pleroma/dependency_version_test.exs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.DependencyVersionTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
test "uses majic 1.2" do
|
||||
majic_version =
|
||||
:majic
|
||||
|> Application.spec(:vsn)
|
||||
|> to_string()
|
||||
|
||||
assert Version.match?(majic_version, "~> 1.2")
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.ContentLanguageMapTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators.ContentLanguageMap
|
||||
|
||||
test "it validates" do
|
||||
data = %{
|
||||
"en-US" => "mew mew",
|
||||
"en-GB" => "meow meow"
|
||||
}
|
||||
|
||||
assert {:ok, ^data} = ContentLanguageMap.cast(data)
|
||||
end
|
||||
|
||||
test "it validates empty strings" do
|
||||
data = %{
|
||||
"en-US" => "mew mew",
|
||||
"en-GB" => ""
|
||||
}
|
||||
|
||||
assert {:ok, ^data} = ContentLanguageMap.cast(data)
|
||||
end
|
||||
|
||||
test "it ignores non-strings within the map" do
|
||||
data = %{
|
||||
"en-US" => "mew mew",
|
||||
"en-GB" => 123
|
||||
}
|
||||
|
||||
assert {:ok, validated_data} = ContentLanguageMap.cast(data)
|
||||
|
||||
assert validated_data == %{"en-US" => "mew mew"}
|
||||
end
|
||||
|
||||
test "it ignores bad locale codes" do
|
||||
data = %{
|
||||
"en-US" => "mew mew",
|
||||
"en_GB" => "meow meow",
|
||||
"en<<#@!$#!@%!GB" => "meow meow"
|
||||
}
|
||||
|
||||
assert {:ok, validated_data} = ContentLanguageMap.cast(data)
|
||||
|
||||
assert validated_data == %{"en-US" => "mew mew"}
|
||||
end
|
||||
|
||||
test "it complains with non-map data" do
|
||||
assert :error = ContentLanguageMap.cast("mew")
|
||||
assert :error = ContentLanguageMap.cast(["mew"])
|
||||
assert :error = ContentLanguageMap.cast([%{"en-US" => "mew"}])
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCodeTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode
|
||||
|
||||
test "it accepts language code" do
|
||||
text = "pl"
|
||||
assert {:ok, ^text} = LanguageCode.cast(text)
|
||||
end
|
||||
|
||||
test "it accepts language code with region" do
|
||||
text = "pl-PL"
|
||||
assert {:ok, ^text} = LanguageCode.cast(text)
|
||||
end
|
||||
|
||||
test "errors for invalid language code" do
|
||||
assert {:error, :invalid_language} = LanguageCode.cast("ru_RU")
|
||||
assert {:error, :invalid_language} = LanguageCode.cast(" ")
|
||||
assert {:error, :invalid_language} = LanguageCode.cast("en-US\n")
|
||||
end
|
||||
|
||||
test "errors for non-text" do
|
||||
assert :error == LanguageCode.cast(42)
|
||||
end
|
||||
end
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
defmodule Pleroma.Emoji.PackTest do
|
||||
use Pleroma.DataCase
|
||||
alias Pleroma.Emoji
|
||||
alias Pleroma.Emoji.Pack
|
||||
|
||||
@emoji_path Path.join(
|
||||
|
|
@ -12,6 +13,9 @@ defmodule Pleroma.Emoji.PackTest do
|
|||
)
|
||||
|
||||
setup do
|
||||
# Reload emoji to ensure a clean state
|
||||
Emoji.reload()
|
||||
|
||||
pack_path = Path.join(@emoji_path, "dump_pack")
|
||||
File.mkdir(pack_path)
|
||||
|
||||
|
|
@ -53,6 +57,63 @@ defmodule Pleroma.Emoji.PackTest do
|
|||
|
||||
assert updated_pack.files_count == 5
|
||||
end
|
||||
|
||||
test "skips existing emojis when adding from zip file", %{pack: pack} do
|
||||
# First, let's create a test pack with a "bear" emoji
|
||||
test_pack_path = Path.join(@emoji_path, "test_bear_pack")
|
||||
Pleroma.Backports.mkdir_p(test_pack_path)
|
||||
|
||||
# Create a pack.json file
|
||||
File.write!(Path.join(test_pack_path, "pack.json"), """
|
||||
{
|
||||
"files": { "bear": "bear.png" },
|
||||
"pack": {
|
||||
"description": "Bear Pack", "homepage": "https://pleroma.social",
|
||||
"license": "Test license", "share-files": true
|
||||
}}
|
||||
""")
|
||||
|
||||
# Copy a test image to use as the bear emoji
|
||||
File.cp!(
|
||||
Path.absname("test/instance_static/emoji/test_pack/blank.png"),
|
||||
Path.join(test_pack_path, "bear.png")
|
||||
)
|
||||
|
||||
# Load the pack to register the "bear" emoji in the global registry
|
||||
{:ok, _bear_pack} = Pleroma.Emoji.Pack.load_pack("test_bear_pack")
|
||||
|
||||
# Reload emoji to make sure the bear emoji is in the global registry
|
||||
Emoji.reload()
|
||||
|
||||
# Verify that the bear emoji exists in the global registry
|
||||
assert Emoji.exist?("bear")
|
||||
|
||||
# Now try to add a zip file that contains an emoji with the same shortcode
|
||||
file = %Plug.Upload{
|
||||
content_type: "application/zip",
|
||||
filename: "emojis.zip",
|
||||
path: Path.absname("test/fixtures/emojis.zip")
|
||||
}
|
||||
|
||||
{:ok, updated_pack} = Pack.add_file(pack, nil, nil, file)
|
||||
|
||||
# Verify that the "bear" emoji was skipped
|
||||
refute Map.has_key?(updated_pack.files, "bear")
|
||||
|
||||
# Other emojis should be added
|
||||
assert Map.has_key?(updated_pack.files, "a_trusted_friend-128")
|
||||
assert Map.has_key?(updated_pack.files, "auroraborealis")
|
||||
assert Map.has_key?(updated_pack.files, "baby_in_a_box")
|
||||
assert Map.has_key?(updated_pack.files, "bear-128")
|
||||
|
||||
# Total count should be 4 (all emojis except "bear")
|
||||
assert updated_pack.files_count == 4
|
||||
|
||||
# Clean up the test pack
|
||||
on_exit(fn ->
|
||||
File.rm_rf!(test_pack_path)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
test "returns error when zip file is bad", %{pack: pack} do
|
||||
|
|
@ -62,7 +123,7 @@ defmodule Pleroma.Emoji.PackTest do
|
|||
path: Path.absname("test/instance_static/emoji/test_pack/blank.png")
|
||||
}
|
||||
|
||||
assert Pack.add_file(pack, nil, nil, file) == {:error, :einval}
|
||||
assert {:error, _} = Pack.add_file(pack, nil, nil, file)
|
||||
end
|
||||
|
||||
test "returns pack when zip file is empty", %{pack: pack} do
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ defmodule Pleroma.FrontendTest do
|
|||
@dir "test/frontend_static_test"
|
||||
|
||||
setup do
|
||||
File.mkdir_p!(@dir)
|
||||
Pleroma.Backports.mkdir_p!(@dir)
|
||||
clear_config([:instance, :static_dir], @dir)
|
||||
|
||||
on_exit(fn ->
|
||||
|
|
@ -46,7 +46,7 @@ defmodule Pleroma.FrontendTest do
|
|||
|
||||
folder = Path.join([@dir, "frontends", "pleroma", "fantasy"])
|
||||
previously_existing = Path.join([folder, "temp"])
|
||||
File.mkdir_p!(folder)
|
||||
Pleroma.Backports.mkdir_p!(folder)
|
||||
File.write!(previously_existing, "yey")
|
||||
assert File.exists?(previously_existing)
|
||||
|
||||
|
|
|
|||
179
test/pleroma/gun/conn_test.exs
Normal file
179
test/pleroma/gun/conn_test.exs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Gun.ConnTest do
|
||||
use ExUnit.Case
|
||||
|
||||
import Mox
|
||||
|
||||
alias Pleroma.Gun.Conn
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
setup do
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http} end)
|
||||
stub(Pleroma.GunMock, :await_tunnel_up, fn _, _, _ -> {:ok, :http2} end)
|
||||
stub(Pleroma.GunMock, :close, fn _ -> :ok end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "selects the direct transport from the URI scheme" do
|
||||
expect(Pleroma.GunMock, :open, 2, fn
|
||||
~c"example.com", 8443, %{transport: :tls, tls_opts: tls_opts} ->
|
||||
assert tls_opts[:verify] == :verify_peer
|
||||
{:ok, spawn_link(fn -> Process.sleep(:infinity) end)}
|
||||
|
||||
~c"example.com", 443, %{transport: :tcp} ->
|
||||
{:ok, spawn_link(fn -> Process.sleep(:infinity) end)}
|
||||
end)
|
||||
|
||||
assert {:ok, https_conn, :http, nil} = Conn.open(URI.parse("https://example.com:8443"), [])
|
||||
assert {:ok, http_conn, :http, nil} = Conn.open(URI.parse("http://example.com:443"), [])
|
||||
|
||||
stop_conn(https_conn)
|
||||
stop_conn(http_conn)
|
||||
end
|
||||
|
||||
test "passes authentication to an HTTP CONNECT proxy" do
|
||||
conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||
stream = make_ref()
|
||||
|
||||
expect(Pleroma.GunMock, :open, fn ~c"proxy.example", 8080, opts ->
|
||||
assert opts.transport == :tcp
|
||||
refute Map.has_key?(opts, :tls_opts)
|
||||
{:ok, conn}
|
||||
end)
|
||||
|
||||
expect(Pleroma.GunMock, :connect, fn ^conn, connect_opts ->
|
||||
assert connect_opts.username == "alice"
|
||||
assert connect_opts.password == "secret"
|
||||
assert connect_opts.transport == :tls
|
||||
assert connect_opts.protocols == [:http2, :http]
|
||||
stream
|
||||
end)
|
||||
|
||||
expect(Pleroma.GunMock, :await, fn ^conn, ^stream -> {:response, :fin, 200, []} end)
|
||||
|
||||
assert {:ok, ^conn, :http2, ^stream} =
|
||||
Conn.open(URI.parse("https://origin.example/inbox"),
|
||||
proxy: {~c"proxy.example", 8080},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
)
|
||||
|
||||
stop_conn(conn)
|
||||
end
|
||||
|
||||
test "uses separate TLS options for a TLS CONNECT proxy" do
|
||||
conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||
stream = make_ref()
|
||||
|
||||
expect(Pleroma.GunMock, :open, fn ~c"proxy.example", 8443, opts ->
|
||||
assert opts.transport == :tls
|
||||
assert opts.tls_opts == [verify: :verify_none]
|
||||
{:ok, conn}
|
||||
end)
|
||||
|
||||
expect(Pleroma.GunMock, :connect, fn ^conn, _connect_opts -> stream end)
|
||||
expect(Pleroma.GunMock, :await, fn ^conn, ^stream -> {:response, :fin, 200, []} end)
|
||||
|
||||
assert {:ok, ^conn, :http2, ^stream} =
|
||||
Conn.open(URI.parse("https://origin.example/inbox"),
|
||||
proxy: {~c"proxy.example", 8443},
|
||||
transport: :tls,
|
||||
proxy_tls_opts: [verify: :verify_none]
|
||||
)
|
||||
|
||||
stop_conn(conn)
|
||||
end
|
||||
|
||||
test "uses HTTP/1 for a TLS forwarding proxy" do
|
||||
conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||
|
||||
expect(Pleroma.GunMock, :open, fn ~c"proxy.example", 8443, opts ->
|
||||
assert opts.transport == :tls
|
||||
assert opts.tls_opts == [verify: :verify_none]
|
||||
assert opts.protocols == [:http]
|
||||
{:ok, conn}
|
||||
end)
|
||||
|
||||
assert {:ok, ^conn, :http, :forward_proxy} =
|
||||
Conn.open(URI.parse("http://origin.example/inbox"),
|
||||
proxy: {~c"proxy.example", 8443},
|
||||
transport: :tls,
|
||||
proxy_tls_opts: [verify: :verify_none]
|
||||
)
|
||||
|
||||
stop_conn(conn)
|
||||
end
|
||||
|
||||
test "passes authentication and remote DNS destination to a SOCKS5 proxy" do
|
||||
conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||
|
||||
expect(Pleroma.GunMock, :open, fn ~c"proxy.example", 1080, opts ->
|
||||
assert opts.protocols == [:socks]
|
||||
assert opts.transport == :tcp
|
||||
assert opts.socks_opts.host == ~c"origin.example"
|
||||
assert opts.socks_opts.port == 443
|
||||
assert opts.socks_opts.auth == [{:username_password, "alice", "secret"}]
|
||||
assert opts.socks_opts.transport == :tls
|
||||
assert opts.socks_opts.protocols == [:http2, :http]
|
||||
refute Map.has_key?(opts, :tls_opts)
|
||||
{:ok, conn}
|
||||
end)
|
||||
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :socks} end)
|
||||
|
||||
assert {:ok, ^conn, :http2, nil} =
|
||||
Conn.open(URI.parse("https://origin.example/inbox"),
|
||||
proxy: {:socks5, ~c"proxy.example", 1080},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
)
|
||||
|
||||
stop_conn(conn)
|
||||
end
|
||||
|
||||
test "closes a CONNECT socket when proxy authentication fails" do
|
||||
conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||
stream = make_ref()
|
||||
|
||||
expect(Pleroma.GunMock, :open, fn _, _, _ -> {:ok, conn} end)
|
||||
expect(Pleroma.GunMock, :connect, fn ^conn, _ -> stream end)
|
||||
expect(Pleroma.GunMock, :await, fn ^conn, ^stream -> {:response, :nofin, 407, []} end)
|
||||
expect(Pleroma.GunMock, :close, fn ^conn -> Process.exit(conn, :normal) end)
|
||||
|
||||
assert {:error, :proxy_auth_failed} =
|
||||
Conn.open(URI.parse("https://origin.example/inbox"),
|
||||
proxy: {~c"proxy.example", 8080},
|
||||
proxy_auth: {"alice", "wrong"}
|
||||
)
|
||||
end
|
||||
|
||||
test "returns the proxy status when CONNECT is forbidden" do
|
||||
conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||
stream = make_ref()
|
||||
|
||||
expect(Pleroma.GunMock, :open, fn _, _, _ -> {:ok, conn} end)
|
||||
expect(Pleroma.GunMock, :connect, fn ^conn, _ -> stream end)
|
||||
expect(Pleroma.GunMock, :await, fn ^conn, ^stream -> {:response, :fin, 403, []} end)
|
||||
expect(Pleroma.GunMock, :close, fn ^conn -> Process.exit(conn, :normal) end)
|
||||
|
||||
assert {:error, {:proxy_connect_failed, 403}} =
|
||||
Conn.open(URI.parse("https://origin.example/inbox"),
|
||||
proxy: {~c"proxy.example", 8080}
|
||||
)
|
||||
end
|
||||
|
||||
test "rejects SOCKS4 before opening a socket" do
|
||||
assert {:error, :socks4_unsupported} =
|
||||
Conn.open(URI.parse("https://origin.example/inbox"),
|
||||
proxy: {:socks4, ~c"proxy.example", 1080}
|
||||
)
|
||||
end
|
||||
|
||||
defp stop_conn(conn) do
|
||||
Process.unlink(conn)
|
||||
Process.exit(conn, :kill)
|
||||
end
|
||||
end
|
||||
|
|
@ -11,8 +11,11 @@ defmodule Pleroma.Gun.ConnectionPoolTest do
|
|||
|
||||
defp gun_mock(_) do
|
||||
Pleroma.GunMock
|
||||
|> stub(:open, fn _, _, _ -> Task.start_link(fn -> Process.sleep(100) end) end)
|
||||
|> stub(:open, fn _, _, _ -> Task.start_link(fn -> Process.sleep(:infinity) end) end)
|
||||
|> stub(:await_up, fn _, _ -> {:ok, :http} end)
|
||||
|> stub(:connect, fn _, _ -> make_ref() end)
|
||||
|> stub(:await, fn _, _ -> {:response, :fin, 200, []} end)
|
||||
|> stub(:await_tunnel_up, fn _, _, _ -> {:ok, :http2} end)
|
||||
|> stub(:set_owner, fn _, _ -> :ok end)
|
||||
|
||||
:ok
|
||||
|
|
@ -20,41 +23,213 @@ defmodule Pleroma.Gun.ConnectionPoolTest do
|
|||
|
||||
setup :gun_mock
|
||||
|
||||
test "gives the same connection to 2 concurrent requests" do
|
||||
Enum.map(
|
||||
[
|
||||
"http://www.korean-books.com.kp/KBMbooks/en/periodic/pictorial/20200530163914.pdf",
|
||||
"http://www.korean-books.com.kp/KBMbooks/en/periodic/pictorial/20200528183427.pdf"
|
||||
],
|
||||
fn uri ->
|
||||
uri = URI.parse(uri)
|
||||
task_parent = self()
|
||||
test "opens sibling HTTP/1 connections while an existing connection is in use" do
|
||||
uri = URI.parse("http://www.korean-books.com.kp/document.pdf")
|
||||
|
||||
Task.start_link(fn ->
|
||||
{:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
ConnectionPool.release_conn(conn)
|
||||
send(task_parent, conn)
|
||||
end)
|
||||
end
|
||||
)
|
||||
assert {:ok, first} = ConnectionPool.get_conn(uri, [])
|
||||
assert {:ok, second} = ConnectionPool.get_conn(uri, [])
|
||||
assert first != second
|
||||
|
||||
[pid, pid] =
|
||||
for _ <- 1..2 do
|
||||
receive do
|
||||
pid -> pid
|
||||
end
|
||||
end
|
||||
assert :ok = ConnectionPool.release_conn(first)
|
||||
assert :ok = ConnectionPool.release_conn(second)
|
||||
end
|
||||
|
||||
test "multiplexes concurrent HTTP/2 leases on one connection" do
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http2} end)
|
||||
uri = URI.parse("https://h2.example/inbox")
|
||||
|
||||
assert {:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
assert {:ok, ^conn} = ConnectionPool.get_conn(uri, [])
|
||||
|
||||
assert :ok = ConnectionPool.release_conn(conn)
|
||||
assert :ok = ConnectionPool.release_conn(conn)
|
||||
assert Process.alive?(conn)
|
||||
end
|
||||
|
||||
test "does not share connections across direct and authenticated proxy routes" do
|
||||
uri = URI.parse("https://routes.example/inbox")
|
||||
|
||||
assert {:ok, direct} = ConnectionPool.get_conn(uri, [])
|
||||
|
||||
assert {:ok, proxied} =
|
||||
ConnectionPool.get_conn(uri,
|
||||
proxy: {~c"proxy.example", 8080},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
)
|
||||
|
||||
assert {:ok, other_credentials} =
|
||||
ConnectionPool.get_conn(uri,
|
||||
proxy: {~c"proxy.example", 8080},
|
||||
proxy_auth: {"bob", "different-secret"}
|
||||
)
|
||||
|
||||
assert Enum.uniq([direct, proxied, other_credentials]) ==
|
||||
[direct, proxied, other_credentials]
|
||||
|
||||
registry_dump = inspect(Registry.select(ConnectionPool, [{{:"$1", :_, :_}, [], [:"$1"]}]))
|
||||
refute registry_dump =~ "secret"
|
||||
|
||||
Enum.each([direct, proxied, other_credentials], &ConnectionPool.release_conn/1)
|
||||
end
|
||||
|
||||
test "coalesces concurrent cold HTTP/2 checkouts behind one connection" do
|
||||
test_pid = self()
|
||||
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ ->
|
||||
send(test_pid, {:awaiting_connection, self()})
|
||||
|
||||
receive do
|
||||
:continue -> {:ok, :http2}
|
||||
end
|
||||
end)
|
||||
|
||||
uri = URI.parse("https://cold-h2.example/inbox")
|
||||
|
||||
tasks =
|
||||
for _ <- 1..10 do
|
||||
Task.async(fn -> ConnectionPool.get_conn(uri, []) end)
|
||||
end
|
||||
|
||||
assert_receive {:awaiting_connection, worker_pid}
|
||||
refute_receive {:awaiting_connection, _other_worker}, 50
|
||||
send(worker_pid, :continue)
|
||||
|
||||
assert [{:ok, conn}] = tasks |> Enum.map(&Task.await/1) |> Enum.uniq()
|
||||
assert Process.alive?(conn)
|
||||
end
|
||||
|
||||
test "shares a cold connection failure with concurrent waiters" do
|
||||
test_pid = self()
|
||||
|
||||
stub(Pleroma.GunMock, :open, fn _, _, _ ->
|
||||
send(test_pid, {:opening_connection, self()})
|
||||
|
||||
receive do
|
||||
:fail -> {:error, :econnrefused}
|
||||
end
|
||||
end)
|
||||
|
||||
uri = URI.parse("https://unreachable.example/inbox")
|
||||
|
||||
tasks =
|
||||
for _ <- 1..10 do
|
||||
Task.async(fn -> ConnectionPool.get_conn(uri, []) end)
|
||||
end
|
||||
|
||||
assert_receive {:opening_connection, worker_pid}
|
||||
refute_receive {:opening_connection, _other_worker}, 50
|
||||
send(worker_pid, :fail)
|
||||
|
||||
assert Enum.all?(tasks, fn task -> match?({:error, _reason}, Task.await(task)) end)
|
||||
refute_receive {:opening_connection, _other_worker}, 50
|
||||
end
|
||||
|
||||
test "bounds many-host workloads and closes released idle workers" do
|
||||
clear_config([:connections_pool, :max_connections]) do
|
||||
clear_config([:connections_pool, :max_connections], 25)
|
||||
clear_config([:connections_pool, :max_idle_time], 5)
|
||||
restart_worker_supervisor()
|
||||
|
||||
on_exit(&restart_worker_supervisor/0)
|
||||
end
|
||||
|
||||
connections =
|
||||
Enum.map(1..25, fn host_number ->
|
||||
uri = URI.parse("https://host-#{host_number}.example/inbox")
|
||||
assert {:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
conn
|
||||
end)
|
||||
|
||||
assert DynamicSupervisor.count_children(Pleroma.Gun.ConnectionPool.WorkerSupervisor).active ==
|
||||
25
|
||||
|
||||
assert {:error, :pool_full} =
|
||||
ConnectionPool.get_conn(URI.parse("https://overflow.example/inbox"), [])
|
||||
|
||||
Enum.each(connections, &ConnectionPool.release_conn/1)
|
||||
|
||||
assert eventually(fn ->
|
||||
DynamicSupervisor.count_children(Pleroma.Gun.ConnectionPool.WorkerSupervisor).active ==
|
||||
0
|
||||
end)
|
||||
end
|
||||
|
||||
test "does not reclaim a worker that became active after idle selection" do
|
||||
uri = URI.parse("https://reclaim-race.example/inbox")
|
||||
|
||||
assert {:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
assert :ok = ConnectionPool.release_conn(conn)
|
||||
worker = worker_for_conn(conn)
|
||||
|
||||
assert {:idle, _crf, _last_reference} = GenServer.call(worker, :reclaim_info)
|
||||
assert {:ok, ^conn} = ConnectionPool.get_conn(uri, [])
|
||||
assert :in_use = GenServer.call(worker, :reclaim)
|
||||
assert Process.alive?(worker)
|
||||
|
||||
assert :ok = ConnectionPool.release_conn(conn)
|
||||
end
|
||||
|
||||
test "cancels one HTTP/2 stream without discarding the connection" do
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http2} end)
|
||||
uri = URI.parse("https://cancel-h2.example/inbox")
|
||||
stream = make_ref()
|
||||
|
||||
expect(Pleroma.GunMock, :cancel, fn conn, ^stream when is_pid(conn) -> :ok end)
|
||||
|
||||
assert {:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
assert :ok = ConnectionPool.register_stream(conn, stream)
|
||||
assert :ok = ConnectionPool.cancel_stream(conn, stream)
|
||||
assert {:ok, ^conn} = ConnectionPool.get_conn(uri, [])
|
||||
assert :ok = ConnectionPool.release_conn(conn)
|
||||
end
|
||||
|
||||
test "ignores a stale Gun error after cancelling an HTTP/2 stream" do
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http2} end)
|
||||
uri = URI.parse("https://stale-cancel-h2.example/inbox")
|
||||
stream = make_ref()
|
||||
|
||||
expect(Pleroma.GunMock, :cancel, fn conn, ^stream when is_pid(conn) -> :ok end)
|
||||
|
||||
assert {:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
worker = worker_for_conn(conn)
|
||||
assert :ok = ConnectionPool.register_stream(conn, stream)
|
||||
assert :ok = ConnectionPool.cancel_stream(conn, stream)
|
||||
|
||||
send(worker, {:gun_error, conn, stream, {:badstate, "The stream cannot be found."}})
|
||||
assert %{clients: %{}} = :sys.get_state(worker)
|
||||
assert Process.alive?(worker)
|
||||
|
||||
assert {:ok, ^conn} = ConnectionPool.get_conn(uri, [])
|
||||
assert :ok = ConnectionPool.release_conn(conn)
|
||||
end
|
||||
|
||||
test "stops an HTTP/1 worker when a client dies with an active stream" do
|
||||
uri = URI.parse("http://dead-stream.example/media")
|
||||
parent = self()
|
||||
|
||||
client =
|
||||
spawn(fn ->
|
||||
{:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
:ok = ConnectionPool.register_stream(conn, make_ref())
|
||||
send(parent, {:active_stream, conn})
|
||||
Process.sleep(:infinity)
|
||||
end)
|
||||
|
||||
assert_receive {:active_stream, conn}
|
||||
worker = worker_for_conn(conn)
|
||||
Process.exit(client, :kill)
|
||||
|
||||
assert eventually(fn -> not Process.alive?(worker) end)
|
||||
end
|
||||
|
||||
@tag :erratic
|
||||
test "connection limit is respected with concurrent requests" do
|
||||
clear_config([:connections_pool, :max_connections]) do
|
||||
clear_config([:connections_pool, :max_connections], 1)
|
||||
# The supervisor needs a reboot to apply the new config setting
|
||||
Process.exit(Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor), :kill)
|
||||
restart_worker_supervisor()
|
||||
|
||||
on_exit(fn ->
|
||||
Process.exit(Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor), :kill)
|
||||
restart_worker_supervisor()
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
@ -97,4 +272,41 @@ defmodule Pleroma.Gun.ConnectionPoolTest do
|
|||
|> Enum.sort()
|
||||
end)
|
||||
end
|
||||
|
||||
defp restart_worker_supervisor do
|
||||
supervisor = Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor)
|
||||
monitor = Process.monitor(supervisor)
|
||||
Process.exit(supervisor, :kill)
|
||||
|
||||
assert_receive {:DOWN, ^monitor, :process, ^supervisor, :killed}
|
||||
|
||||
assert eventually(fn ->
|
||||
case Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor) do
|
||||
pid when is_pid(pid) -> pid != supervisor and Process.alive?(pid)
|
||||
nil -> false
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp worker_for_conn(conn) do
|
||||
[worker] =
|
||||
Registry.select(ConnectionPool, [
|
||||
{{:_, :"$1", {:"$2", :_}}, [{:==, :"$2", conn}], [:"$1"]}
|
||||
])
|
||||
|
||||
worker
|
||||
end
|
||||
|
||||
defp eventually(predicate, attempts \\ 50)
|
||||
|
||||
defp eventually(_predicate, 0), do: false
|
||||
|
||||
defp eventually(predicate, attempts) do
|
||||
if predicate.() do
|
||||
true
|
||||
else
|
||||
Process.sleep(10)
|
||||
eventually(predicate, attempts - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,4 +14,133 @@ defmodule Pleroma.HashtagTest do
|
|||
assert {:name, {"can't be blank", [validation: :required]}} in changeset.errors
|
||||
end
|
||||
end
|
||||
|
||||
describe "search_hashtags" do
|
||||
test "searches hashtags by partial match" do
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("car")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("racecar")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("nascar")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("bicycle")
|
||||
|
||||
results = Hashtag.search("car")
|
||||
assert "car" in results
|
||||
assert "racecar" in results
|
||||
assert "nascar" in results
|
||||
refute "bicycle" in results
|
||||
|
||||
results = Hashtag.search("race")
|
||||
assert "racecar" in results
|
||||
refute "car" in results
|
||||
refute "nascar" in results
|
||||
refute "bicycle" in results
|
||||
|
||||
results = Hashtag.search("nonexistent")
|
||||
assert results == []
|
||||
end
|
||||
|
||||
test "searches hashtags by multiple words in query" do
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("computer")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("laptop")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("desktop")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("phone")
|
||||
|
||||
# Search for "new computer" - should return "computer"
|
||||
results = Hashtag.search("new computer")
|
||||
assert "computer" in results
|
||||
refute "laptop" in results
|
||||
refute "desktop" in results
|
||||
refute "phone" in results
|
||||
|
||||
# Search for "computer laptop" - should return both
|
||||
results = Hashtag.search("computer laptop")
|
||||
assert "computer" in results
|
||||
assert "laptop" in results
|
||||
refute "desktop" in results
|
||||
refute "phone" in results
|
||||
|
||||
# Search for "new phone" - should return "phone"
|
||||
results = Hashtag.search("new phone")
|
||||
assert "phone" in results
|
||||
refute "computer" in results
|
||||
refute "laptop" in results
|
||||
refute "desktop" in results
|
||||
end
|
||||
|
||||
test "supports pagination" do
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("alpha")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("beta")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("gamma")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("delta")
|
||||
|
||||
results = Hashtag.search("a", limit: 2)
|
||||
assert length(results) == 2
|
||||
|
||||
results = Hashtag.search("a", limit: 2, offset: 1)
|
||||
assert length(results) == 2
|
||||
end
|
||||
|
||||
test "handles matching many search terms" do
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("computer")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("laptop")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("phone")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("tablet")
|
||||
|
||||
results = Hashtag.search("new fast computer laptop phone tablet device")
|
||||
assert "computer" in results
|
||||
assert "laptop" in results
|
||||
assert "phone" in results
|
||||
assert "tablet" in results
|
||||
end
|
||||
|
||||
test "ranks results by match quality" do
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("my_computer")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("computer_science")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("computer")
|
||||
|
||||
results = Hashtag.search("computer")
|
||||
|
||||
# Exact match first
|
||||
assert Enum.at(results, 0) == "computer"
|
||||
|
||||
# Prefix match would be next
|
||||
assert Enum.at(results, 1) == "computer_science"
|
||||
|
||||
# worst match is last
|
||||
assert Enum.at(results, 2) == "my_computer"
|
||||
end
|
||||
|
||||
test "prioritizes shorter names when ranking is equal" do
|
||||
# Create hashtags with same ranking but different lengths
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("car")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("racecar")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("nascar")
|
||||
|
||||
# Search for "car" - shorter names should come first
|
||||
results = Hashtag.search("car")
|
||||
# Shortest exact match first
|
||||
assert Enum.at(results, 0) == "car"
|
||||
assert "racecar" in results
|
||||
assert "nascar" in results
|
||||
end
|
||||
|
||||
test "handles hashtag symbols in search query" do
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("computer")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("laptop")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("phone")
|
||||
|
||||
results_with_hash = Hashtag.search("#computer #laptop")
|
||||
results_without_hash = Hashtag.search("computer laptop")
|
||||
|
||||
assert results_with_hash == results_without_hash
|
||||
|
||||
results_mixed = Hashtag.search("#computer laptop #phone")
|
||||
assert "computer" in results_mixed
|
||||
assert "laptop" in results_mixed
|
||||
assert "phone" in results_mixed
|
||||
|
||||
results_only_hash = Hashtag.search("#computer")
|
||||
results_no_hash = Hashtag.search("computer")
|
||||
assert results_only_hash == results_no_hash
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ defmodule Pleroma.HTMLTest do
|
|||
<span class="h-card"><a class="u-url mention animate-spin">@<span>foo</span></a></span>
|
||||
"""
|
||||
|
||||
@mention_hashtags_sample """
|
||||
<a href="https://mastodon.example/tags/linux" class="mention hashtag" rel="tag">#<span>linux</span></a>
|
||||
"""
|
||||
|
||||
describe "StripTags scrubber" do
|
||||
test "works as expected" do
|
||||
expected = """
|
||||
|
|
@ -126,6 +130,15 @@ defmodule Pleroma.HTMLTest do
|
|||
Pleroma.HTML.Scrubber.TwitterText
|
||||
)
|
||||
end
|
||||
|
||||
test "does allow mention hashtags" do
|
||||
expected = """
|
||||
<a href="https://mastodon.example/tags/linux" class="mention hashtag" rel="tag">#<span>linux</span></a>
|
||||
"""
|
||||
|
||||
assert expected ==
|
||||
HTML.filter_tags(@mention_hashtags_sample, Pleroma.HTML.Scrubber.Default)
|
||||
end
|
||||
end
|
||||
|
||||
describe "default scrubber" do
|
||||
|
|
@ -189,6 +202,15 @@ defmodule Pleroma.HTMLTest do
|
|||
Pleroma.HTML.Scrubber.Default
|
||||
)
|
||||
end
|
||||
|
||||
test "does allow mention hashtags" do
|
||||
expected = """
|
||||
<a href="https://mastodon.example/tags/linux" class="mention hashtag" rel="tag">#<span>linux</span></a>
|
||||
"""
|
||||
|
||||
assert expected ==
|
||||
HTML.filter_tags(@mention_hashtags_sample, Pleroma.HTML.Scrubber.Default)
|
||||
end
|
||||
end
|
||||
|
||||
describe "extract_first_external_url_from_object" do
|
||||
|
|
|
|||
|
|
@ -55,23 +55,29 @@ defmodule Pleroma.HTTP.AdapterHelper.GunTest do
|
|||
|
||||
uri = URI.parse("https://some-domain.com")
|
||||
opts = Gun.options([receive_conn: false], uri)
|
||||
assert opts[:proxy] == {'localhost', 8123}
|
||||
assert opts[:proxy] == {~c"localhost", 8123}
|
||||
end
|
||||
|
||||
test "parses tuple proxy scheme host and port" do
|
||||
clear_config([:http, :proxy_url], {:socks, 'localhost', 1234})
|
||||
clear_config([:http, :proxy_url], {:socks, ~c"localhost", 1234})
|
||||
|
||||
uri = URI.parse("https://some-domain.com")
|
||||
opts = Gun.options([receive_conn: false], uri)
|
||||
assert opts[:proxy] == {:socks, 'localhost', 1234}
|
||||
assert opts[:proxy] == {:socks, ~c"localhost", 1234}
|
||||
end
|
||||
|
||||
test "passed opts have more weight than defaults" do
|
||||
clear_config([:http, :proxy_url], {:socks5, 'localhost', 1234})
|
||||
clear_config([:http, :proxy_url], {:socks5, ~c"localhost", 1234})
|
||||
uri = URI.parse("https://some-domain.com")
|
||||
opts = Gun.options([receive_conn: false, proxy: {'example.com', 4321}], uri)
|
||||
opts = Gun.options([receive_conn: false, proxy: {~c"example.com", 4321}], uri)
|
||||
|
||||
assert opts[:proxy] == {'example.com', 4321}
|
||||
assert opts[:proxy] == {~c"example.com", 4321}
|
||||
end
|
||||
|
||||
test "uses managed chunks for streamed responses" do
|
||||
opts = Gun.options([stream: true], URI.parse("https://example.com"))
|
||||
|
||||
assert opts[:body_as] == :chunks
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,6 +16,14 @@ defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do
|
|||
describe "options/2" do
|
||||
setup do: clear_config([:http, :adapter], a: 1, b: 2)
|
||||
|
||||
test "uses redirect-safe defaults", %{uri: uri} do
|
||||
opts = Hackney.options([], uri)
|
||||
|
||||
assert opts[:follow_redirect] == false
|
||||
assert opts[:force_redirect] == false
|
||||
assert opts[:with_body] == true
|
||||
end
|
||||
|
||||
test "add proxy and opts from config", %{uri: uri} do
|
||||
opts = Hackney.options([proxy: "localhost:8123"], uri)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,12 +17,12 @@ defmodule Pleroma.HTTP.AdapterHelperTest do
|
|||
end
|
||||
|
||||
test "localhost with port" do
|
||||
assert AdapterHelper.format_proxy("localhost:8123") == {'localhost', 8123}
|
||||
assert AdapterHelper.format_proxy("localhost:8123") == {~c"localhost", 8123}
|
||||
end
|
||||
|
||||
test "tuple" do
|
||||
assert AdapterHelper.format_proxy({:socks4, :localhost, 9050}) ==
|
||||
{:socks4, 'localhost', 9050}
|
||||
{:socks4, ~c"localhost", 9050}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
808
test/pleroma/http/gun_integration_test.exs
Normal file
808
test/pleroma/http/gun_integration_test.exs
Normal file
|
|
@ -0,0 +1,808 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.GunIntegrationTest do
|
||||
use ExUnit.Case, async: false
|
||||
use Pleroma.Tests.Helpers
|
||||
|
||||
alias Pleroma.Tesla.Middleware.ConnectionPool
|
||||
|
||||
setup do
|
||||
clear_config([Pleroma.Gun], Pleroma.Gun.API)
|
||||
clear_config([:connections_pool, :max_idle_time], 1_000)
|
||||
|
||||
origin = start_tls_origin(self())
|
||||
connect_proxy = start_connect_proxy(self())
|
||||
forward_proxy = start_forward_proxy(self())
|
||||
socks_proxy = start_socks5_proxy(self())
|
||||
|
||||
on_exit(fn ->
|
||||
Process.sleep(20)
|
||||
stop_server(socks_proxy)
|
||||
stop_server(forward_proxy)
|
||||
stop_server(connect_proxy)
|
||||
stop_server(origin)
|
||||
end)
|
||||
|
||||
{:ok,
|
||||
origin: origin,
|
||||
connect_proxy: connect_proxy,
|
||||
forward_proxy: forward_proxy,
|
||||
socks_proxy: socks_proxy}
|
||||
end
|
||||
|
||||
test "requests HTTPS directly on a non-standard port", %{origin: origin} do
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(origin.base_url <> "/final")
|
||||
end
|
||||
|
||||
test "follows redirects through an authenticated CONNECT proxy", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
|
||||
request(origin.base_url <> "/redirect", proxy_opts, true)
|
||||
|
||||
expected = "Basic " <> Base.encode64("alice:secret")
|
||||
assert_receive {:connect_proxy, ^expected, "127.0.0.1", origin_port}
|
||||
assert origin_port == origin.port
|
||||
end
|
||||
|
||||
test "requests through an authenticated CONNECT proxy", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
result = request(origin.base_url <> "/final", proxy_opts)
|
||||
|
||||
expected = "Basic " <> Base.encode64("alice:secret")
|
||||
assert_receive {:connect_proxy, ^expected, "127.0.0.1", origin_port}
|
||||
assert origin_port == origin.port
|
||||
assert worker_protocol(origin.port) == :http
|
||||
assert_receive {:origin_request, "/final"}
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = result
|
||||
end
|
||||
|
||||
test "requests HTTP through a forwarding proxy", %{forward_proxy: proxy} do
|
||||
proxy_opts = [proxy: {~c"127.0.0.1", proxy.port}]
|
||||
url = "http://origin.example:8080/final?key=value"
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(url, proxy_opts)
|
||||
|
||||
assert_receive {:forward_proxy, ^url, "origin.example:8080", nil}
|
||||
end
|
||||
|
||||
test "authenticates HTTP requests to a forwarding proxy", %{forward_proxy: proxy} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
url = "http://origin.example/final"
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(url, proxy_opts)
|
||||
|
||||
expected = "Basic " <> Base.encode64("alice:secret")
|
||||
assert_receive {:forward_proxy, ^url, "origin.example", ^expected}
|
||||
end
|
||||
|
||||
test "opens a request-ready authenticated CONNECT tunnel", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
uri = URI.parse(origin.base_url <> "/final")
|
||||
|
||||
opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"},
|
||||
tls_opts: [verify: :verify_none],
|
||||
connect_timeout: 2_000
|
||||
]
|
||||
|
||||
assert {:ok, conn, :http, tunnel} = Pleroma.Gun.Conn.open(uri, opts)
|
||||
stream = :gun.get(conn, "/final", [], %{tunnel: tunnel})
|
||||
assert {:response, :nofin, 200, _headers} = :gun.await(conn, stream, 2_000)
|
||||
assert {:ok, "ok"} = :gun.await_body(conn, stream, 2_000)
|
||||
:gun.close(conn)
|
||||
end
|
||||
|
||||
test "streams chunks through an authenticated CONNECT tunnel", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: client}} =
|
||||
request_chunks(origin.base_url <> "/chunks", proxy_opts)
|
||||
|
||||
assert collect_chunks(client) == "hello"
|
||||
assert :ok = Pleroma.Gun.ConnectionPool.release_conn(client.pid)
|
||||
end
|
||||
|
||||
test "discards an unfinished HTTP/1 redirect stream before following it", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: client}} =
|
||||
request_chunks(origin.base_url <> "/redirect_chunks", proxy_opts, true)
|
||||
|
||||
assert collect_chunks(client) == "hello"
|
||||
assert :ok = Pleroma.Gun.ConnectionPool.release_conn(client.pid)
|
||||
|
||||
expected = "Basic " <> Base.encode64("alice:secret")
|
||||
assert_receive {:connect_proxy, ^expected, "127.0.0.1", _port}
|
||||
assert_receive {:connect_proxy, ^expected, "127.0.0.1", _port}
|
||||
end
|
||||
|
||||
test "uploads multipart bodies through an authenticated CONNECT tunnel", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
multipart = Tesla.Multipart.new() |> Tesla.Multipart.add_field("name", "value")
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200}} =
|
||||
request_multipart(origin.base_url <> "/upload", multipart, proxy_opts)
|
||||
|
||||
assert_receive {:origin_headers, "/upload", headers}
|
||||
assert String.downcase(headers) =~ "content-type: multipart/form-data;"
|
||||
assert_receive {:origin_body, "/upload", body}
|
||||
assert body =~ "name=\"name\""
|
||||
assert body =~ "value"
|
||||
end
|
||||
|
||||
test "uploads streamed bodies through an authenticated CONNECT tunnel", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
body = Stream.map(["streamed", "-body"], & &1)
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200}} =
|
||||
request_stream(origin.base_url <> "/upload", body, proxy_opts)
|
||||
|
||||
assert_receive {:origin_body, "/upload", "streamed-body"}
|
||||
end
|
||||
|
||||
test "discards a partial HTTP/1 upload when body enumeration raises", %{origin: origin} do
|
||||
body =
|
||||
Stream.map(["partial", "raise"], fn
|
||||
"raise" -> raise "upload failed"
|
||||
part -> part
|
||||
end)
|
||||
|
||||
assert_raise RuntimeError, "upload failed", fn ->
|
||||
request_stream(origin.base_url <> "/upload", body, [])
|
||||
end
|
||||
|
||||
assert_receive {:origin_headers, "/upload", _headers}
|
||||
assert worker_protocol(origin.port) == nil
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(origin.base_url <> "/final")
|
||||
end
|
||||
|
||||
test "releases the lease when Gun rejects an invalid request header", %{origin: origin} do
|
||||
adapter_opts = [
|
||||
tls_opts: [verify: :verify_none],
|
||||
connect_timeout: 2_000,
|
||||
timeout: 2_000
|
||||
]
|
||||
|
||||
client = Tesla.client([ConnectionPool], Tesla.Adapter.Gun)
|
||||
|
||||
assert {:invalid_request_header, "x-test", _message} =
|
||||
catch_error(
|
||||
Tesla.get(client, origin.base_url <> "/final",
|
||||
headers: [{"x-test", "value\r\nx-injected: true"}],
|
||||
opts: [adapter: adapter_opts]
|
||||
)
|
||||
)
|
||||
|
||||
assert worker_clients(origin.port) == %{}
|
||||
refute_receive {:origin_request, "/final"}
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(origin.base_url <> "/final")
|
||||
end
|
||||
|
||||
test "requests through authenticated SOCKS5 with proxy-side DNS", %{
|
||||
origin: origin,
|
||||
socks_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {:socks5, ~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
url = "https://localhost:#{origin.port}/final"
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(url, proxy_opts)
|
||||
|
||||
assert_receive {:socks5_proxy, "alice", "secret", "localhost", origin_port}
|
||||
assert origin_port == origin.port
|
||||
end
|
||||
|
||||
test "requests through unauthenticated SOCKS5 with an IP proxy host", %{
|
||||
origin: origin,
|
||||
socks_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [proxy: {:socks5, {127, 0, 0, 1}, proxy.port}]
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
|
||||
request(origin.base_url <> "/final", proxy_opts)
|
||||
|
||||
assert_receive {:socks5_proxy, nil, nil, "127.0.0.1", origin_port}
|
||||
assert origin_port == origin.port
|
||||
end
|
||||
|
||||
test "requests through unauthenticated SOCKS5 with a localhost proxy host", %{
|
||||
origin: origin,
|
||||
socks_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [proxy: {:socks5, ~c"localhost", proxy.port}]
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
|
||||
request(origin.base_url <> "/final", proxy_opts)
|
||||
|
||||
assert_receive {:socks5_proxy, nil, nil, "127.0.0.1", origin_port}
|
||||
assert origin_port == origin.port
|
||||
end
|
||||
|
||||
defp request(url, proxy_opts \\ [], follow_redirects \\ false) do
|
||||
middleware =
|
||||
if follow_redirects do
|
||||
[Tesla.Middleware.FollowRedirects, ConnectionPool]
|
||||
else
|
||||
[ConnectionPool]
|
||||
end
|
||||
|
||||
adapter_opts =
|
||||
Keyword.merge(
|
||||
[tls_opts: [verify: :verify_none], connect_timeout: 2_000, timeout: 2_000],
|
||||
proxy_opts
|
||||
)
|
||||
|
||||
middleware
|
||||
|> Tesla.client(Tesla.Adapter.Gun)
|
||||
|> Tesla.get(url, opts: [adapter: adapter_opts])
|
||||
end
|
||||
|
||||
defp request_chunks(url, proxy_opts, follow_redirects \\ false) do
|
||||
adapter_opts =
|
||||
Keyword.merge(
|
||||
[
|
||||
body_as: :chunks,
|
||||
tls_opts: [verify: :verify_none],
|
||||
connect_timeout: 2_000,
|
||||
timeout: 2_000
|
||||
],
|
||||
proxy_opts
|
||||
)
|
||||
|
||||
middleware =
|
||||
if follow_redirects do
|
||||
[Tesla.Middleware.FollowRedirects, ConnectionPool]
|
||||
else
|
||||
[ConnectionPool]
|
||||
end
|
||||
|
||||
middleware
|
||||
|> Tesla.client(Tesla.Adapter.Gun)
|
||||
|> Tesla.get(url, opts: [adapter: adapter_opts])
|
||||
end
|
||||
|
||||
defp request_multipart(url, multipart, proxy_opts) do
|
||||
adapter_opts =
|
||||
Keyword.merge(
|
||||
[tls_opts: [verify: :verify_none], connect_timeout: 2_000, timeout: 2_000],
|
||||
proxy_opts
|
||||
)
|
||||
|
||||
[ConnectionPool]
|
||||
|> Tesla.client(Tesla.Adapter.Gun)
|
||||
|> Tesla.post(url, multipart, opts: [adapter: adapter_opts])
|
||||
end
|
||||
|
||||
defp request_stream(url, body, proxy_opts) do
|
||||
adapter_opts =
|
||||
Keyword.merge(
|
||||
[tls_opts: [verify: :verify_none], connect_timeout: 2_000, timeout: 2_000],
|
||||
proxy_opts
|
||||
)
|
||||
|
||||
[ConnectionPool]
|
||||
|> Tesla.client(Tesla.Adapter.Gun)
|
||||
|> Tesla.post(url, body, opts: [adapter: adapter_opts])
|
||||
end
|
||||
|
||||
defp collect_chunks(client, acc \\ "") do
|
||||
case Tesla.Adapter.Gun.read_chunk(client.pid, client.stream, client.opts) do
|
||||
{:nofin, chunk} -> collect_chunks(client, acc <> chunk)
|
||||
{:fin, chunk} -> acc <> chunk
|
||||
end
|
||||
end
|
||||
|
||||
defp start_tls_origin(parent) do
|
||||
certfile = Path.expand("../../fixtures/server.pem", __DIR__)
|
||||
keyfile = Path.expand("../../fixtures/private_key.pem", __DIR__)
|
||||
|
||||
{:ok, listener} =
|
||||
:ssl.listen(0, [
|
||||
:binary,
|
||||
certfile: certfile,
|
||||
keyfile: keyfile,
|
||||
reuseaddr: true,
|
||||
active: false,
|
||||
packet: :raw,
|
||||
ip: {127, 0, 0, 1}
|
||||
])
|
||||
|
||||
{:ok, {{127, 0, 0, 1}, port}} = :ssl.sockname(listener)
|
||||
{:ok, acceptor} = Task.start_link(fn -> accept_tls_loop(listener, parent) end)
|
||||
|
||||
%{
|
||||
listener: listener,
|
||||
acceptor: acceptor,
|
||||
transport: :ssl,
|
||||
port: port,
|
||||
base_url: "https://127.0.0.1:#{port}"
|
||||
}
|
||||
end
|
||||
|
||||
defp accept_tls_loop(listener, parent) do
|
||||
case :ssl.transport_accept(listener) do
|
||||
{:ok, tcp_socket} ->
|
||||
case :ssl.handshake(tcp_socket, 2_000) do
|
||||
{:ok, socket} ->
|
||||
pid = spawn(fn -> receive_and_serve_tls(socket, parent) end)
|
||||
:ok = :ssl.controlling_process(socket, pid)
|
||||
send(pid, :serve)
|
||||
|
||||
{:error, _reason} ->
|
||||
:gen_tcp.close(tcp_socket)
|
||||
end
|
||||
|
||||
accept_tls_loop(listener, parent)
|
||||
|
||||
{:error, _reason} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp receive_and_serve_tls(socket, parent) do
|
||||
receive do
|
||||
:serve -> serve_tls(socket, parent)
|
||||
end
|
||||
end
|
||||
|
||||
defp serve_tls(socket, parent) do
|
||||
case recv_headers(:ssl, socket) do
|
||||
{:ok, request} ->
|
||||
{headers, buffered_body} = split_headers(request)
|
||||
path = request_path(headers)
|
||||
send(parent, {:origin_request, path})
|
||||
send(parent, {:origin_headers, path, headers})
|
||||
|
||||
case path do
|
||||
"/redirect" ->
|
||||
send_tls_response(socket, 302, "Found", [{"Location", "/final"}], "")
|
||||
|
||||
"/redirect_chunks" ->
|
||||
send_tls_response(socket, 302, "Found", [{"Location", "/chunks"}], "discard-me")
|
||||
|
||||
"/final" ->
|
||||
send_tls_response(socket, 200, "OK", [], "ok")
|
||||
|
||||
"/chunks" ->
|
||||
send_tls_chunks(socket, ["hel", "lo"])
|
||||
|
||||
"/upload" ->
|
||||
{:ok, body} = recv_request_body(socket, headers, buffered_body)
|
||||
send(parent, {:origin_body, path, body})
|
||||
send_tls_response(socket, 200, "OK", [], "uploaded")
|
||||
|
||||
_ ->
|
||||
send_tls_response(socket, 404, "Not Found", [], "not found")
|
||||
end
|
||||
|
||||
serve_tls(socket, parent)
|
||||
|
||||
{:error, _reason} ->
|
||||
:ssl.close(socket)
|
||||
end
|
||||
end
|
||||
|
||||
defp send_tls_response(socket, status, reason, headers, body) do
|
||||
headers =
|
||||
[
|
||||
{"Content-Length", Integer.to_string(byte_size(body))},
|
||||
{"Connection", "keep-alive"}
|
||||
] ++ headers
|
||||
|
||||
:ssl.send(socket, [
|
||||
"HTTP/1.1 ",
|
||||
Integer.to_string(status),
|
||||
" ",
|
||||
reason,
|
||||
"\r\n",
|
||||
Enum.map(headers, fn {key, value} -> [key, ": ", value, "\r\n"] end),
|
||||
"\r\n",
|
||||
body
|
||||
])
|
||||
end
|
||||
|
||||
defp send_tls_chunks(socket, chunks) do
|
||||
:ssl.send(socket, [
|
||||
"HTTP/1.1 200 OK\r\n",
|
||||
"Transfer-Encoding: chunked\r\n",
|
||||
"Connection: keep-alive\r\n\r\n",
|
||||
Enum.map(chunks, fn chunk ->
|
||||
[Integer.to_string(byte_size(chunk), 16), "\r\n", chunk, "\r\n"]
|
||||
end),
|
||||
"0\r\n\r\n"
|
||||
])
|
||||
end
|
||||
|
||||
defp start_connect_proxy(parent) do
|
||||
start_tcp_server(fn socket -> serve_connect_proxy(socket, parent) end)
|
||||
end
|
||||
|
||||
defp serve_connect_proxy(socket, parent) do
|
||||
with {:ok, headers} <- recv_headers(:gen_tcp, socket),
|
||||
{:ok, host, port} <- parse_connect(headers),
|
||||
auth when is_binary(auth) <- header(headers, "proxy-authorization"),
|
||||
{:ok, upstream} <- tcp_connect(host, port) do
|
||||
send(parent, {:connect_proxy, auth, host, port})
|
||||
:ok = :gen_tcp.send(socket, "HTTP/1.1 200 Connection established\r\n\r\n")
|
||||
tunnel(socket, upstream)
|
||||
else
|
||||
_ ->
|
||||
:gen_tcp.send(
|
||||
socket,
|
||||
"HTTP/1.1 407 Proxy Authentication Required\r\nContent-Length: 0\r\n\r\n"
|
||||
)
|
||||
|
||||
:gen_tcp.close(socket)
|
||||
end
|
||||
end
|
||||
|
||||
defp start_forward_proxy(parent) do
|
||||
start_tcp_server(fn socket -> serve_forward_proxy(socket, parent) end)
|
||||
end
|
||||
|
||||
defp serve_forward_proxy(socket, parent) do
|
||||
with {:ok, request} <- recv_headers(:gen_tcp, socket) do
|
||||
{headers, _buffered_body} = split_headers(request)
|
||||
target = request_path(headers)
|
||||
host = header(headers, "host")
|
||||
auth = header(headers, "proxy-authorization")
|
||||
|
||||
if String.starts_with?(target, "http://") do
|
||||
send(parent, {:forward_proxy, target, host, auth})
|
||||
|
||||
:gen_tcp.send(
|
||||
socket,
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: keep-alive\r\n\r\nok"
|
||||
)
|
||||
|
||||
serve_forward_proxy(socket, parent)
|
||||
else
|
||||
:gen_tcp.send(
|
||||
socket,
|
||||
"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
||||
)
|
||||
|
||||
:gen_tcp.close(socket)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp start_socks5_proxy(parent) do
|
||||
start_tcp_server(fn socket -> serve_socks5_proxy(socket, parent) end)
|
||||
end
|
||||
|
||||
defp serve_socks5_proxy(socket, parent) do
|
||||
with {:ok, <<5, method_count>>} <- :gen_tcp.recv(socket, 2, 2_000),
|
||||
{:ok, methods} <- :gen_tcp.recv(socket, method_count, 2_000),
|
||||
{:ok, username, password} <- negotiate_socks5_auth(socket, methods),
|
||||
{:ok, host, port} <- recv_socks5_destination(socket),
|
||||
{:ok, upstream} <- tcp_connect(host, port),
|
||||
:ok <- :gen_tcp.send(socket, <<5, 0, 0, 1, 0, 0, 0, 0, 0, 0>>) do
|
||||
send(parent, {:socks5_proxy, username, password, host, port})
|
||||
tunnel(socket, upstream)
|
||||
else
|
||||
_ ->
|
||||
:gen_tcp.close(socket)
|
||||
end
|
||||
end
|
||||
|
||||
defp negotiate_socks5_auth(socket, methods) do
|
||||
cond do
|
||||
:binary.match(methods, <<2>>) != :nomatch ->
|
||||
with :ok <- :gen_tcp.send(socket, <<5, 2>>),
|
||||
{:ok, username, password} <- recv_socks5_auth(socket),
|
||||
:ok <- :gen_tcp.send(socket, <<1, 0>>) do
|
||||
{:ok, username, password}
|
||||
end
|
||||
|
||||
:binary.match(methods, <<0>>) != :nomatch ->
|
||||
with :ok <- :gen_tcp.send(socket, <<5, 0>>) do
|
||||
{:ok, nil, nil}
|
||||
end
|
||||
|
||||
true ->
|
||||
{:error, :unsupported_auth}
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_socks5_auth(socket) do
|
||||
with {:ok, <<1, username_length>>} <- :gen_tcp.recv(socket, 2, 2_000),
|
||||
{:ok, username} <- :gen_tcp.recv(socket, username_length, 2_000),
|
||||
{:ok, <<password_length>>} <- :gen_tcp.recv(socket, 1, 2_000),
|
||||
{:ok, password} <- :gen_tcp.recv(socket, password_length, 2_000) do
|
||||
{:ok, username, password}
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_socks5_destination(socket) do
|
||||
with {:ok, <<5, 1, 0, address_type>>} <- :gen_tcp.recv(socket, 4, 2_000),
|
||||
{:ok, host} <- recv_socks5_host(socket, address_type),
|
||||
{:ok, <<port::16>>} <- :gen_tcp.recv(socket, 2, 2_000) do
|
||||
{:ok, host, port}
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_socks5_host(socket, 1) do
|
||||
with {:ok, <<a, b, c, d>>} <- :gen_tcp.recv(socket, 4, 2_000) do
|
||||
{:ok, Enum.join([a, b, c, d], ".")}
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_socks5_host(socket, 3) do
|
||||
with {:ok, <<length>>} <- :gen_tcp.recv(socket, 1, 2_000),
|
||||
{:ok, host} <- :gen_tcp.recv(socket, length, 2_000) do
|
||||
{:ok, host}
|
||||
end
|
||||
end
|
||||
|
||||
defp start_tcp_server(handler) 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_link(fn -> accept_tcp_loop(listener, handler) end)
|
||||
%{listener: listener, acceptor: acceptor, transport: :gen_tcp, port: port}
|
||||
end
|
||||
|
||||
defp accept_tcp_loop(listener, handler) do
|
||||
case :gen_tcp.accept(listener) do
|
||||
{:ok, socket} ->
|
||||
pid = spawn(fn -> receive_and_serve_tcp(socket, handler) end)
|
||||
:ok = :gen_tcp.controlling_process(socket, pid)
|
||||
send(pid, :serve)
|
||||
accept_tcp_loop(listener, handler)
|
||||
|
||||
{:error, _reason} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp receive_and_serve_tcp(socket, handler) do
|
||||
receive do
|
||||
:serve -> handler.(socket)
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_headers(transport, socket, acc \\ <<>>) do
|
||||
case transport.recv(socket, 0, 2_000) do
|
||||
{:ok, data} ->
|
||||
acc = acc <> data
|
||||
|
||||
if :binary.match(acc, "\r\n\r\n") == :nomatch do
|
||||
recv_headers(transport, socket, acc)
|
||||
else
|
||||
{:ok, acc}
|
||||
end
|
||||
|
||||
{:error, _reason} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp request_path(headers) do
|
||||
headers
|
||||
|> String.split("\r\n", parts: 2)
|
||||
|> hd()
|
||||
|> String.split(" ")
|
||||
|> Enum.at(1)
|
||||
end
|
||||
|
||||
defp split_headers(request) do
|
||||
{index, 4} = :binary.match(request, "\r\n\r\n")
|
||||
split_at = index + 4
|
||||
<<headers::binary-size(split_at), body::binary>> = request
|
||||
{headers, body}
|
||||
end
|
||||
|
||||
defp recv_request_body(socket, headers, buffered_body) do
|
||||
cond do
|
||||
String.contains?(String.downcase(headers), "transfer-encoding: chunked") ->
|
||||
recv_chunked_body(socket, buffered_body, "")
|
||||
|
||||
content_length = header(headers, "content-length") ->
|
||||
recv_exact(socket, buffered_body, String.to_integer(content_length))
|
||||
|
||||
true ->
|
||||
{:ok, buffered_body}
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_chunked_body(socket, buffer, acc) do
|
||||
with {:ok, line, buffer} <- recv_line(socket, buffer),
|
||||
{size, ""} <- Integer.parse(line, 16) do
|
||||
if size == 0 do
|
||||
{:ok, acc}
|
||||
else
|
||||
with {:ok, chunk_and_crlf, buffer} <- take_bytes(socket, buffer, size + 2),
|
||||
<<chunk::binary-size(size), "\r\n">> <- chunk_and_crlf do
|
||||
recv_chunked_body(socket, buffer, acc <> chunk)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_line(socket, buffer) do
|
||||
case :binary.match(buffer, "\r\n") do
|
||||
{index, 2} ->
|
||||
<<line::binary-size(index), "\r\n", rest::binary>> = buffer
|
||||
{:ok, line, rest}
|
||||
|
||||
:nomatch ->
|
||||
with {:ok, data} <- :ssl.recv(socket, 0, 2_000) do
|
||||
recv_line(socket, buffer <> data)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp take_bytes(_socket, buffer, size) when byte_size(buffer) >= size do
|
||||
<<bytes::binary-size(size), rest::binary>> = buffer
|
||||
{:ok, bytes, rest}
|
||||
end
|
||||
|
||||
defp take_bytes(socket, buffer, size) do
|
||||
with {:ok, data} <- :ssl.recv(socket, 0, 2_000) do
|
||||
take_bytes(socket, buffer <> data, size)
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_exact(socket, buffer, size) do
|
||||
with {:ok, body, _rest} <- take_bytes(socket, buffer, size) do
|
||||
{:ok, body}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_connect(headers) do
|
||||
case headers |> String.split("\r\n", parts: 2) |> hd() |> String.split(" ") do
|
||||
["CONNECT", authority, _protocol] ->
|
||||
case String.split(authority, ":", parts: 2) do
|
||||
[host, port] -> {:ok, host, String.to_integer(port)}
|
||||
_ -> {:error, :invalid_authority}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, :invalid_connect}
|
||||
end
|
||||
end
|
||||
|
||||
defp header(headers, wanted_name) do
|
||||
headers
|
||||
|> String.split("\r\n")
|
||||
|> Enum.find_value(fn line ->
|
||||
case String.split(line, ":", parts: 2) do
|
||||
[name, value] ->
|
||||
if String.downcase(name) == wanted_name, do: String.trim(value)
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp tcp_connect(host, port) do
|
||||
:gen_tcp.connect(
|
||||
String.to_charlist(host),
|
||||
port,
|
||||
[:binary, active: false, packet: :raw],
|
||||
2_000
|
||||
)
|
||||
end
|
||||
|
||||
defp worker_protocol(port) do
|
||||
case worker_state(port) do
|
||||
nil -> nil
|
||||
state -> state.protocol
|
||||
end
|
||||
end
|
||||
|
||||
defp worker_clients(port) do
|
||||
case worker_state(port) do
|
||||
nil -> nil
|
||||
state -> state.clients
|
||||
end
|
||||
end
|
||||
|
||||
defp worker_state(port) do
|
||||
prefix = "https:127.0.0.1:#{port}:"
|
||||
|
||||
Pleroma.Gun.ConnectionPool
|
||||
|> Registry.select([{{:"$1", :"$2", :_}, [], [{{:"$1", :"$2"}}]}])
|
||||
|> Enum.find_value(fn {key, worker} ->
|
||||
if String.starts_with?(key, prefix), do: :sys.get_state(worker)
|
||||
end)
|
||||
end
|
||||
|
||||
defp tunnel(client, upstream) do
|
||||
:ok = :inet.setopts(client, active: :once)
|
||||
:ok = :inet.setopts(upstream, active: :once)
|
||||
tunnel_loop(client, upstream)
|
||||
end
|
||||
|
||||
defp tunnel_loop(client, upstream) do
|
||||
receive do
|
||||
{:tcp, ^client, data} ->
|
||||
:ok = :gen_tcp.send(upstream, data)
|
||||
:ok = :inet.setopts(client, active: :once)
|
||||
tunnel_loop(client, upstream)
|
||||
|
||||
{:tcp, ^upstream, data} ->
|
||||
:ok = :gen_tcp.send(client, data)
|
||||
:ok = :inet.setopts(upstream, active: :once)
|
||||
tunnel_loop(client, upstream)
|
||||
|
||||
{:tcp_closed, _socket} ->
|
||||
:gen_tcp.close(client)
|
||||
:gen_tcp.close(upstream)
|
||||
|
||||
{:tcp_error, _socket, _reason} ->
|
||||
:gen_tcp.close(client)
|
||||
:gen_tcp.close(upstream)
|
||||
after
|
||||
10_000 ->
|
||||
:gen_tcp.close(client)
|
||||
:gen_tcp.close(upstream)
|
||||
end
|
||||
end
|
||||
|
||||
defp stop_server(%{listener: listener, acceptor: acceptor, transport: transport}) do
|
||||
transport.close(listener)
|
||||
|
||||
if Process.alive?(acceptor), do: Process.exit(acceptor, :normal)
|
||||
end
|
||||
end
|
||||
404
test/pleroma/http/hackney_follow_redirect_regression_test.exs
Normal file
404
test/pleroma/http/hackney_follow_redirect_regression_test.exs
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
setup do
|
||||
{:ok, _} = Application.ensure_all_started(:hackney)
|
||||
|
||||
{:ok, tls_server} = start_tls_redirect_server()
|
||||
{:ok, proxy} = start_connect_proxy()
|
||||
|
||||
on_exit(fn ->
|
||||
stop_connect_proxy(proxy)
|
||||
stop_tls_redirect_server(tls_server)
|
||||
end)
|
||||
|
||||
{: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
|
||||
} do
|
||||
url = "#{tls_server.base_url}/redirect"
|
||||
|
||||
adapter_opts = [
|
||||
pool: :media,
|
||||
proxy: proxy.proxy_url,
|
||||
insecure: true,
|
||||
connect_timeout: 1_000,
|
||||
recv_timeout: 1_000,
|
||||
follow_redirect: false,
|
||||
force_redirect: false,
|
||||
with_body: true
|
||||
]
|
||||
|
||||
client = Tesla.client([Tesla.Middleware.FollowRedirects], Tesla.Adapter.Hackney)
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
|
||||
Tesla.request(client, method: :get, url: url, opts: [adapter: adapter_opts])
|
||||
end
|
||||
|
||||
test "reverse proxy hackney client follows redirects via proxy without crashing", %{
|
||||
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
|
||||
]
|
||||
|
||||
assert {:ok, 200, _headers, ref} =
|
||||
Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts)
|
||||
|
||||
assert collect_body(ref) == "ok"
|
||||
Pleroma.ReverseProxy.Client.Hackney.close(ref)
|
||||
end
|
||||
|
||||
test "hackney reverse proxy follows nested redirects via proxy", %{
|
||||
tls_server: tls_server,
|
||||
proxy: proxy
|
||||
} do
|
||||
url = "#{tls_server.base_url}/nested_redirect"
|
||||
|
||||
opts = [
|
||||
pool: :media,
|
||||
proxy: proxy.proxy_url,
|
||||
insecure: true,
|
||||
connect_timeout: 1_000,
|
||||
recv_timeout: 1_000,
|
||||
follow_redirect: true
|
||||
]
|
||||
|
||||
assert {:ok, 200, _headers, ref} =
|
||||
Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts)
|
||||
|
||||
assert collect_body(ref) == "ok"
|
||||
Pleroma.ReverseProxy.Client.Hackney.close(ref)
|
||||
end
|
||||
|
||||
test "hackney reverse proxy stops following redirects after limit is reached", %{
|
||||
tls_server: tls_server,
|
||||
proxy: proxy
|
||||
} do
|
||||
url = "#{tls_server.base_url}/infinite_redirect"
|
||||
|
||||
opts = [
|
||||
pool: :media,
|
||||
proxy: proxy.proxy_url,
|
||||
insecure: true,
|
||||
connect_timeout: 1_000,
|
||||
recv_timeout: 1_000,
|
||||
follow_redirect: true
|
||||
]
|
||||
|
||||
assert {:ok, 302, _headers, ref} =
|
||||
Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts)
|
||||
|
||||
Pleroma.ReverseProxy.Client.Hackney.close(ref)
|
||||
end
|
||||
|
||||
defp collect_body(ref, acc \\ "") do
|
||||
case Pleroma.ReverseProxy.Client.Hackney.stream_body(ref) do
|
||||
:done -> acc
|
||||
{:ok, data, _ref} -> collect_body(ref, acc <> data)
|
||||
{:error, error} -> flunk("stream_body failed: #{inspect(error)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp start_tls_redirect_server do
|
||||
certfile = Path.expand("../../fixtures/server.pem", __DIR__)
|
||||
keyfile = Path.expand("../../fixtures/private_key.pem", __DIR__)
|
||||
|
||||
{:ok, listener} =
|
||||
:ssl.listen(0, [
|
||||
:binary,
|
||||
certfile: certfile,
|
||||
keyfile: keyfile,
|
||||
reuseaddr: true,
|
||||
active: false,
|
||||
packet: :raw,
|
||||
ip: {127, 0, 0, 1}
|
||||
])
|
||||
|
||||
{:ok, {{127, 0, 0, 1}, port}} = :ssl.sockname(listener)
|
||||
|
||||
{:ok, acceptor} =
|
||||
Task.start_link(fn ->
|
||||
accept_tls_loop(listener)
|
||||
end)
|
||||
|
||||
{:ok, %{listener: listener, acceptor: acceptor, base_url: "https://127.0.0.1:#{port}"}}
|
||||
end
|
||||
|
||||
defp stop_tls_redirect_server(%{listener: listener, acceptor: acceptor}) do
|
||||
:ok = :ssl.close(listener)
|
||||
|
||||
if Process.alive?(acceptor) do
|
||||
Process.exit(acceptor, :normal)
|
||||
end
|
||||
end
|
||||
|
||||
defp accept_tls_loop(listener) do
|
||||
case :ssl.transport_accept(listener) do
|
||||
{:ok, socket} ->
|
||||
_ = Task.start(fn -> serve_tls(socket) end)
|
||||
accept_tls_loop(listener)
|
||||
|
||||
{:error, :closed} ->
|
||||
:ok
|
||||
|
||||
{:error, _reason} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp serve_tls(tcp_socket) do
|
||||
with {:ok, ssl_socket} <- :ssl.handshake(tcp_socket, 2_000),
|
||||
{:ok, data} <- recv_ssl_headers(ssl_socket),
|
||||
{:ok, path} <- parse_path(data) do
|
||||
case path do
|
||||
"/infinite_redirect" ->
|
||||
send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/infinite_redirect"}], "")
|
||||
|
||||
"/nested_redirect" ->
|
||||
send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/redirect"}], "")
|
||||
|
||||
"/redirect" ->
|
||||
send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/final"}], "")
|
||||
|
||||
"/final" ->
|
||||
send_ssl_response(ssl_socket, 200, "OK", [], "ok")
|
||||
|
||||
_ ->
|
||||
send_ssl_response(ssl_socket, 404, "Not Found", [], "not found")
|
||||
end
|
||||
|
||||
:ssl.close(ssl_socket)
|
||||
else
|
||||
_ ->
|
||||
_ = :gen_tcp.close(tcp_socket)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_ssl_headers(socket, acc \\ <<>>) do
|
||||
case :ssl.recv(socket, 0, 1_000) do
|
||||
{:ok, data} ->
|
||||
acc = acc <> data
|
||||
|
||||
if :binary.match(acc, "\r\n\r\n") != :nomatch do
|
||||
{:ok, acc}
|
||||
else
|
||||
if byte_size(acc) > 8_192 do
|
||||
{:error, :too_large}
|
||||
else
|
||||
recv_ssl_headers(socket, acc)
|
||||
end
|
||||
end
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp send_ssl_response(socket, status, reason, headers, body) do
|
||||
base_headers =
|
||||
[
|
||||
{"Content-Length", Integer.to_string(byte_size(body))},
|
||||
{"Connection", "close"}
|
||||
] ++ headers
|
||||
|
||||
iodata =
|
||||
[
|
||||
"HTTP/1.1 ",
|
||||
Integer.to_string(status),
|
||||
" ",
|
||||
reason,
|
||||
"\r\n",
|
||||
Enum.map(base_headers, fn {k, v} -> [k, ": ", v, "\r\n"] end),
|
||||
"\r\n",
|
||||
body
|
||||
]
|
||||
|
||||
:ssl.send(socket, iodata)
|
||||
end
|
||||
|
||||
defp start_connect_proxy 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_link(fn ->
|
||||
accept_proxy_loop(listener)
|
||||
end)
|
||||
|
||||
{:ok, %{listener: listener, acceptor: acceptor, proxy_url: "127.0.0.1:#{port}"}}
|
||||
end
|
||||
|
||||
defp stop_connect_proxy(%{listener: listener, acceptor: acceptor}) do
|
||||
:ok = :gen_tcp.close(listener)
|
||||
|
||||
if Process.alive?(acceptor) do
|
||||
Process.exit(acceptor, :normal)
|
||||
end
|
||||
end
|
||||
|
||||
defp accept_proxy_loop(listener) do
|
||||
case :gen_tcp.accept(listener) do
|
||||
{:ok, socket} ->
|
||||
_ = Task.start(fn -> serve_proxy(socket) end)
|
||||
accept_proxy_loop(listener)
|
||||
|
||||
{:error, :closed} ->
|
||||
:ok
|
||||
|
||||
{:error, _reason} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp serve_proxy(client_socket) do
|
||||
with {:ok, {headers, rest}} <- recv_tcp_headers(client_socket),
|
||||
{:ok, {host, port}} <- parse_connect(headers),
|
||||
{:ok, upstream_socket} <- connect_upstream(host, port) do
|
||||
:gen_tcp.send(client_socket, "HTTP/1.1 200 Connection established\r\n\r\n")
|
||||
|
||||
if rest != <<>> do
|
||||
:gen_tcp.send(upstream_socket, rest)
|
||||
end
|
||||
|
||||
tunnel(client_socket, upstream_socket)
|
||||
else
|
||||
_ ->
|
||||
:gen_tcp.close(client_socket)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp tunnel(client_socket, upstream_socket) do
|
||||
parent = self()
|
||||
_ = spawn_link(fn -> forward(client_socket, upstream_socket, parent) end)
|
||||
_ = spawn_link(fn -> forward(upstream_socket, client_socket, parent) end)
|
||||
|
||||
receive do
|
||||
:tunnel_closed -> :ok
|
||||
after
|
||||
10_000 -> :ok
|
||||
end
|
||||
|
||||
:gen_tcp.close(client_socket)
|
||||
:gen_tcp.close(upstream_socket)
|
||||
end
|
||||
|
||||
defp forward(from_socket, to_socket, parent) do
|
||||
case :gen_tcp.recv(from_socket, 0, 10_000) do
|
||||
{:ok, data} ->
|
||||
_ = :gen_tcp.send(to_socket, data)
|
||||
forward(from_socket, to_socket, parent)
|
||||
|
||||
{:error, _reason} ->
|
||||
send(parent, :tunnel_closed)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_tcp_headers(socket, acc \\ <<>>) do
|
||||
case :gen_tcp.recv(socket, 0, 1_000) do
|
||||
{:ok, data} ->
|
||||
acc = acc <> data
|
||||
|
||||
case :binary.match(acc, "\r\n\r\n") do
|
||||
:nomatch ->
|
||||
if byte_size(acc) > 8_192 do
|
||||
{:error, :too_large}
|
||||
else
|
||||
recv_tcp_headers(socket, acc)
|
||||
end
|
||||
|
||||
{idx, _len} ->
|
||||
split_at = idx + 4
|
||||
<<headers::binary-size(split_at), rest::binary>> = acc
|
||||
{:ok, {headers, rest}}
|
||||
end
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_connect(data) do
|
||||
with [request_line | _] <- String.split(data, "\r\n", trim: true),
|
||||
["CONNECT", hostport | _] <- String.split(request_line, " ", parts: 3),
|
||||
[host, port_str] <- String.split(hostport, ":", parts: 2),
|
||||
{port, ""} <- Integer.parse(port_str) do
|
||||
{:ok, {host, port}}
|
||||
else
|
||||
_ -> {:error, :invalid_connect}
|
||||
end
|
||||
end
|
||||
|
||||
defp connect_upstream(host, port) do
|
||||
address =
|
||||
case :inet.parse_address(String.to_charlist(host)) do
|
||||
{:ok, ip} -> ip
|
||||
{:error, _} -> String.to_charlist(host)
|
||||
end
|
||||
|
||||
:gen_tcp.connect(address, port, [:binary, active: false, packet: :raw], 1_000)
|
||||
end
|
||||
|
||||
defp parse_path(data) do
|
||||
case String.split(data, "\r\n", parts: 2) do
|
||||
[request_line | _] ->
|
||||
case String.split(request_line, " ") do
|
||||
[_method, path, _protocol] -> {:ok, path}
|
||||
_ -> {:error, :invalid_request}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, :invalid_request}
|
||||
end
|
||||
end
|
||||
end
|
||||
151
test/pleroma/http/hackney_redirect_regression_test.exs
Normal file
151
test/pleroma/http/hackney_redirect_regression_test.exs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.HackneyRedirectRegressionTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Pleroma.HTTP.AdapterHelper.Hackney, as: HackneyAdapterHelper
|
||||
|
||||
setup do
|
||||
{:ok, _} = Application.ensure_all_started(:hackney)
|
||||
|
||||
{:ok, server} = start_server()
|
||||
on_exit(fn -> stop_server(server) end)
|
||||
|
||||
{:ok, server: server}
|
||||
end
|
||||
|
||||
test "pooled redirects work with follow_redirect disabled", %{server: server} do
|
||||
url = "#{server.base_url}/redirect"
|
||||
uri = URI.parse(url)
|
||||
|
||||
adapter_opts =
|
||||
HackneyAdapterHelper.options(
|
||||
[pool: :media, follow_redirect: false, no_proxy_env: true],
|
||||
uri
|
||||
)
|
||||
|
||||
client = Tesla.client([Tesla.Middleware.FollowRedirects], Tesla.Adapter.Hackney)
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
|
||||
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_link(fn ->
|
||||
accept_loop(listener)
|
||||
end)
|
||||
|
||||
{:ok, %{listener: listener, acceptor: acceptor, base_url: "http://127.0.0.1:#{port}"}}
|
||||
end
|
||||
|
||||
defp stop_server(%{listener: listener, acceptor: acceptor}) do
|
||||
:ok = :gen_tcp.close(listener)
|
||||
|
||||
if Process.alive?(acceptor) do
|
||||
Process.exit(acceptor, :normal)
|
||||
end
|
||||
end
|
||||
|
||||
defp accept_loop(listener) do
|
||||
case :gen_tcp.accept(listener) do
|
||||
{:ok, socket} ->
|
||||
serve(socket)
|
||||
accept_loop(listener)
|
||||
|
||||
{:error, :closed} ->
|
||||
:ok
|
||||
|
||||
{:error, _reason} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp serve(socket) do
|
||||
with {:ok, data} <- recv_headers(socket),
|
||||
{:ok, path} <- parse_path(data) do
|
||||
case path do
|
||||
"/redirect" ->
|
||||
send_response(socket, 302, "Found", [{"Location", "/final"}], "")
|
||||
|
||||
"/final" ->
|
||||
send_response(socket, 200, "OK", [], "ok")
|
||||
|
||||
_ ->
|
||||
send_response(socket, 404, "Not Found", [], "not found")
|
||||
end
|
||||
else
|
||||
_ -> :ok
|
||||
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
|
||||
|
||||
if :binary.match(acc, "\r\n\r\n") != :nomatch do
|
||||
{:ok, acc}
|
||||
else
|
||||
if byte_size(acc) > 8_192 do
|
||||
{:error, :too_large}
|
||||
else
|
||||
recv_headers(socket, acc)
|
||||
end
|
||||
end
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_path(data) do
|
||||
case String.split(data, "\r\n", parts: 2) do
|
||||
[request_line | _] ->
|
||||
case String.split(request_line, " ") do
|
||||
[_method, path, _protocol] -> {:ok, path}
|
||||
_ -> {:error, :invalid_request}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, :invalid_request}
|
||||
end
|
||||
end
|
||||
|
||||
defp send_response(socket, status, reason, headers, body) do
|
||||
base_headers =
|
||||
[
|
||||
{"Content-Length", Integer.to_string(byte_size(body))},
|
||||
{"Connection", "close"}
|
||||
] ++ headers
|
||||
|
||||
iodata =
|
||||
[
|
||||
"HTTP/1.1 ",
|
||||
Integer.to_string(status),
|
||||
" ",
|
||||
reason,
|
||||
"\r\n",
|
||||
Enum.map(base_headers, fn {k, v} -> [k, ": ", v, "\r\n"] end),
|
||||
"\r\n",
|
||||
body
|
||||
]
|
||||
|
||||
:gen_tcp.send(socket, iodata)
|
||||
end
|
||||
end
|
||||
|
|
@ -3,10 +3,13 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTPTest do
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnit.Case, async: false
|
||||
use Pleroma.Tests.Helpers
|
||||
|
||||
import Tesla.Mock
|
||||
|
||||
alias Pleroma.HTTP
|
||||
alias Pleroma.Utils.URIEncoding
|
||||
|
||||
setup do
|
||||
mock(fn
|
||||
|
|
@ -25,6 +28,39 @@ defmodule Pleroma.HTTPTest do
|
|||
|
||||
%{method: :post, url: "http://example.com/world"} ->
|
||||
%Tesla.Env{status: 200, body: "world"}
|
||||
|
||||
%{method: :get, url: "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz"} ->
|
||||
%Tesla.Env{status: 200, body: "emoji data"}
|
||||
|
||||
%{
|
||||
method: :get,
|
||||
url: "https://example.com/media/foo/bar%20!$&'()*+,;=/:%20@a%20%5Bbaz%5D.mp4"
|
||||
} ->
|
||||
%Tesla.Env{status: 200, body: "video data"}
|
||||
|
||||
%{method: :get, url: "https://example.com/media/unicode%20%F0%9F%99%82%20.gif"} ->
|
||||
%Tesla.Env{status: 200, body: "unicode data"}
|
||||
|
||||
%{
|
||||
method: :get,
|
||||
url:
|
||||
"https://i.guim.co.uk/img/media/1069ef13c447908272c4de94174cec2b6352cb2f/0_91_2000_1201/master/2000.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&precrop=40:21,offset-x50,offset-y0&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvdGctb3BpbmlvbnMtYWdlLTIwMTkucG5n&enable=upscale&s=cba21427a73512fdc9863c486c03fdd8"
|
||||
} ->
|
||||
%Tesla.Env{status: 200, body: "Guardian image quirk"}
|
||||
|
||||
%{
|
||||
method: :get,
|
||||
url:
|
||||
"https://i.guim.co.uk/emoji/Pack%201/koronebless.png?precrop=40:21,overlay-x0,overlay-y0&foo=bar+baz"
|
||||
} ->
|
||||
%Tesla.Env{status: 200, body: "Space in query with Guardian quirk"}
|
||||
|
||||
%{
|
||||
method: :get,
|
||||
url:
|
||||
"https://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=accessKEY%2F20130721%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130721T201207Z&X-Amz-Expires=86400&X-Amz-Signature=SIGNATURE&X-Amz-SignedHeaders=host"
|
||||
} ->
|
||||
%Tesla.Env{status: 200, body: "AWS S3 data"}
|
||||
end)
|
||||
|
||||
:ok
|
||||
|
|
@ -67,4 +103,115 @@ defmodule Pleroma.HTTPTest do
|
|||
}
|
||||
end
|
||||
end
|
||||
|
||||
test "URL encoding properly encodes URLs with spaces" do
|
||||
clear_config(:test_url_encoding, true)
|
||||
|
||||
url_with_space = "https://example.com/emoji/Pack 1/koronebless.png?foo=bar baz"
|
||||
|
||||
{:ok, result} = HTTP.get(url_with_space)
|
||||
|
||||
assert result.status == 200
|
||||
|
||||
properly_encoded_url = "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz"
|
||||
|
||||
{:ok, result} = HTTP.get(properly_encoded_url)
|
||||
|
||||
assert result.status == 200
|
||||
|
||||
url_with_reserved_chars = "https://example.com/media/foo/bar !$&'()*+,;=/: @a [baz].mp4"
|
||||
|
||||
{:ok, result} = HTTP.get(url_with_reserved_chars)
|
||||
|
||||
assert result.status == 200
|
||||
|
||||
url_with_unicode = "https://example.com/media/unicode 🙂 .gif"
|
||||
|
||||
{:ok, result} = HTTP.get(url_with_unicode)
|
||||
|
||||
assert result.status == 200
|
||||
end
|
||||
|
||||
test "decodes URL first by default" do
|
||||
clear_config(:test_url_encoding, true)
|
||||
|
||||
normal_url = "https://example.com/media/file%20with%20space.jpg?name=a+space.jpg"
|
||||
|
||||
result = URIEncoding.encode_url(normal_url)
|
||||
|
||||
assert result == "https://example.com/media/file%20with%20space.jpg?name=a+space.jpg"
|
||||
end
|
||||
|
||||
test "doesn't decode URL first when specified" do
|
||||
clear_config(:test_url_encoding, true)
|
||||
|
||||
normal_url = "https://example.com/media/file%20with%20space.jpg"
|
||||
|
||||
result = URIEncoding.encode_url(normal_url, bypass_decode: true)
|
||||
|
||||
assert result == "https://example.com/media/file%2520with%2520space.jpg"
|
||||
end
|
||||
|
||||
test "properly applies Guardian image query quirk" do
|
||||
clear_config(:test_url_encoding, true)
|
||||
|
||||
url =
|
||||
"https://i.guim.co.uk/img/media/1069ef13c447908272c4de94174cec2b6352cb2f/0_91_2000_1201/master/2000.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&precrop=40:21,offset-x50,offset-y0&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvdGctb3BpbmlvbnMtYWdlLTIwMTkucG5n&enable=upscale&s=cba21427a73512fdc9863c486c03fdd8"
|
||||
|
||||
result = URIEncoding.encode_url(url)
|
||||
|
||||
assert result == url
|
||||
|
||||
{:ok, result_get} = HTTP.get(result)
|
||||
|
||||
assert result_get.status == 200
|
||||
end
|
||||
|
||||
test "properly encodes spaces as \"pluses\" in query when using quirks" do
|
||||
clear_config(:test_url_encoding, true)
|
||||
|
||||
url =
|
||||
"https://i.guim.co.uk/emoji/Pack 1/koronebless.png?precrop=40:21,overlay-x0,overlay-y0&foo=bar baz"
|
||||
|
||||
properly_encoded_url =
|
||||
"https://i.guim.co.uk/emoji/Pack%201/koronebless.png?precrop=40:21,overlay-x0,overlay-y0&foo=bar+baz"
|
||||
|
||||
result = URIEncoding.encode_url(url)
|
||||
|
||||
assert result == properly_encoded_url
|
||||
|
||||
{:ok, result_get} = HTTP.get(result)
|
||||
|
||||
assert result_get.status == 200
|
||||
end
|
||||
|
||||
test "properly encode AWS S3 queries" do
|
||||
clear_config(:test_url_encoding, true)
|
||||
|
||||
url =
|
||||
"https://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=accessKEY%2F20130721%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130721T201207Z&X-Amz-Expires=86400&X-Amz-Signature=SIGNATURE&X-Amz-SignedHeaders=host"
|
||||
|
||||
unencoded_url =
|
||||
"https://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=accessKEY/20130721/us-east-1/s3/aws4_request&X-Amz-Date=20130721T201207Z&X-Amz-Expires=86400&X-Amz-Signature=SIGNATURE&X-Amz-SignedHeaders=host"
|
||||
|
||||
result = URIEncoding.encode_url(url)
|
||||
result_unencoded = URIEncoding.encode_url(unencoded_url)
|
||||
|
||||
assert result == url
|
||||
assert result == result_unencoded
|
||||
|
||||
{:ok, result_get} = HTTP.get(result)
|
||||
|
||||
assert result_get.status == 200
|
||||
end
|
||||
|
||||
test "preserves query key order" do
|
||||
clear_config(:test_url_encoding, true)
|
||||
|
||||
url = "https://example.com/foo?hjkl=qwertz&xyz=abc&bar=baz"
|
||||
|
||||
result = URIEncoding.encode_url(url)
|
||||
|
||||
assert result == url
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,19 +3,15 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Instances.InstanceTest do
|
||||
alias Pleroma.Instances
|
||||
alias Pleroma.Instances.Instance
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Tests.ObanHelpers
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
use Oban.Testing, repo: Pleroma.Repo
|
||||
use Pleroma.DataCase
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
import Pleroma.Factory
|
||||
|
||||
setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1)
|
||||
|
||||
describe "set_reachable/1" do
|
||||
test "clears `unreachable_since` of existing matching Instance record having non-nil `unreachable_since`" do
|
||||
unreachable_since = NaiveDateTime.to_iso8601(NaiveDateTime.utc_now())
|
||||
|
|
@ -31,6 +27,32 @@ defmodule Pleroma.Instances.InstanceTest do
|
|||
assert {:ok, instance} = Instance.set_reachable(instance.host)
|
||||
refute instance.unreachable_since
|
||||
end
|
||||
|
||||
test "cancels all ReachabilityWorker jobs for the domain" do
|
||||
domain = "cancelme.example.org"
|
||||
insert(:instance, host: domain, unreachable_since: NaiveDateTime.utc_now())
|
||||
|
||||
# Insert a ReachabilityWorker job for this domain, scheduled 5 minutes in the future
|
||||
scheduled_at = DateTime.add(DateTime.utc_now(), 300, :second)
|
||||
|
||||
{:ok, job} =
|
||||
Pleroma.Workers.ReachabilityWorker.new(
|
||||
%{"domain" => domain, "phase" => "phase_1min", "attempt" => 1},
|
||||
scheduled_at: scheduled_at
|
||||
)
|
||||
|> Oban.insert()
|
||||
|
||||
# Ensure the job is present
|
||||
job = Pleroma.Repo.get(Oban.Job, job.id)
|
||||
assert job
|
||||
|
||||
# Call set_reachable, which should delete the job
|
||||
assert {:ok, _} = Instance.set_reachable(domain)
|
||||
|
||||
# Reload the job and assert it is deleted
|
||||
job = Pleroma.Repo.get(Oban.Job, job.id)
|
||||
refute job
|
||||
end
|
||||
end
|
||||
|
||||
describe "set_unreachable/1" do
|
||||
|
|
@ -145,7 +167,11 @@ defmodule Pleroma.Instances.InstanceTest do
|
|||
end
|
||||
|
||||
test "Doesn't scrapes unreachable instances" do
|
||||
instance = insert(:instance, unreachable_since: Instances.reachability_datetime_threshold())
|
||||
instance =
|
||||
insert(:instance,
|
||||
unreachable_since: NaiveDateTime.utc_now() |> NaiveDateTime.add(-:timer.hours(24))
|
||||
)
|
||||
|
||||
url = "https://" <> instance.host
|
||||
|
||||
assert capture_log(fn -> assert nil == Instance.get_or_update_favicon(URI.parse(url)) end) =~
|
||||
|
|
@ -213,32 +239,44 @@ defmodule Pleroma.Instances.InstanceTest do
|
|||
end
|
||||
end
|
||||
|
||||
test "delete_users_and_activities/1 deletes remote instance users and activities" do
|
||||
[mario, luigi, _peach, wario] =
|
||||
users = [
|
||||
insert(:user, nickname: "mario@mushroom.kingdom", name: "Mario"),
|
||||
insert(:user, nickname: "luigi@mushroom.kingdom", name: "Luigi"),
|
||||
insert(:user, nickname: "peach@mushroom.kingdom", name: "Peach"),
|
||||
insert(:user, nickname: "wario@greedville.biz", name: "Wario")
|
||||
]
|
||||
test "delete/1 schedules a job to delete the instance and users" do
|
||||
insert(:user, nickname: "mario@mushroom.kingdom", name: "Mario")
|
||||
|
||||
{:ok, post1} = CommonAPI.post(mario, %{status: "letsa go!"})
|
||||
{:ok, post2} = CommonAPI.post(luigi, %{status: "itsa me... luigi"})
|
||||
{:ok, post3} = CommonAPI.post(wario, %{status: "WHA-HA-HA!"})
|
||||
{:ok, _job} = Instance.delete("mushroom.kingdom")
|
||||
|
||||
{:ok, job} = Instance.delete_users_and_activities("mushroom.kingdom")
|
||||
:ok = ObanHelpers.perform(job)
|
||||
assert_enqueued(
|
||||
worker: Pleroma.Workers.DeleteWorker,
|
||||
args: %{"op" => "delete_instance", "host" => "mushroom.kingdom"}
|
||||
)
|
||||
end
|
||||
|
||||
[mario, luigi, peach, wario] = Repo.reload(users)
|
||||
describe "check_unreachable/1" do
|
||||
test "schedules a ReachabilityWorker job for the given domain" do
|
||||
domain = "test.example.com"
|
||||
|
||||
refute mario.is_active
|
||||
refute luigi.is_active
|
||||
refute peach.is_active
|
||||
refute peach.name == "Peach"
|
||||
# Call check_unreachable
|
||||
assert {:ok, _job} = Instance.check_unreachable(domain)
|
||||
|
||||
assert wario.is_active
|
||||
assert wario.name == "Wario"
|
||||
# Verify that a ReachabilityWorker job was scheduled
|
||||
jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker)
|
||||
assert length(jobs) == 1
|
||||
[job] = jobs
|
||||
assert job.args["domain"] == domain
|
||||
end
|
||||
|
||||
assert [nil, nil, %{}] = Repo.reload([post1, post2, post3])
|
||||
test "handles multiple calls for the same domain (uniqueness enforced)" do
|
||||
domain = "duplicate.example.com"
|
||||
|
||||
assert {:ok, _job1} = Instance.check_unreachable(domain)
|
||||
|
||||
# Second call for the same domain
|
||||
assert {:ok, %Oban.Job{conflict?: true}} = Instance.check_unreachable(domain)
|
||||
|
||||
# Should only have one job due to uniqueness
|
||||
jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker)
|
||||
assert length(jobs) == 1
|
||||
[job] = jobs
|
||||
assert job.args["domain"] == domain
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,74 +6,42 @@ defmodule Pleroma.InstancesTest do
|
|||
alias Pleroma.Instances
|
||||
|
||||
use Pleroma.DataCase
|
||||
|
||||
setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1)
|
||||
use Oban.Testing, repo: Pleroma.Repo
|
||||
|
||||
describe "reachable?/1" do
|
||||
test "returns `true` for host / url with unknown reachability status" do
|
||||
assert Instances.reachable?("unknown.site")
|
||||
assert Instances.reachable?("http://unknown.site")
|
||||
end
|
||||
|
||||
test "returns `false` for host / url marked unreachable for at least `reachability_datetime_threshold()`" do
|
||||
host = "consistently-unreachable.name"
|
||||
Instances.set_consistently_unreachable(host)
|
||||
|
||||
refute Instances.reachable?(host)
|
||||
refute Instances.reachable?("http://#{host}/path")
|
||||
end
|
||||
|
||||
test "returns `true` for host / url marked unreachable for less than `reachability_datetime_threshold()`" do
|
||||
url = "http://eventually-unreachable.name/path"
|
||||
|
||||
Instances.set_unreachable(url)
|
||||
|
||||
assert Instances.reachable?(url)
|
||||
assert Instances.reachable?(URI.parse(url).host)
|
||||
end
|
||||
|
||||
test "raises FunctionClauseError exception on non-binary input" do
|
||||
assert_raise FunctionClauseError, fn -> Instances.reachable?(nil) end
|
||||
assert_raise FunctionClauseError, fn -> Instances.reachable?(1) end
|
||||
end
|
||||
end
|
||||
|
||||
describe "filter_reachable/1" do
|
||||
setup do
|
||||
host = "consistently-unreachable.name"
|
||||
url1 = "http://eventually-unreachable.com/path"
|
||||
url2 = "http://domain.com/path"
|
||||
unreachable_host = "consistently-unreachable.name"
|
||||
reachable_host = "http://domain.com/path"
|
||||
|
||||
Instances.set_consistently_unreachable(host)
|
||||
Instances.set_unreachable(url1)
|
||||
Instances.set_unreachable(unreachable_host)
|
||||
|
||||
result = Instances.filter_reachable([host, url1, url2, nil])
|
||||
%{result: result, url1: url1, url2: url2}
|
||||
result = Instances.filter_reachable([unreachable_host, reachable_host, nil])
|
||||
%{result: result, reachable_host: reachable_host, unreachable_host: unreachable_host}
|
||||
end
|
||||
|
||||
test "returns a map with keys containing 'not marked consistently unreachable' elements of supplied list",
|
||||
%{result: result, url1: url1, url2: url2} do
|
||||
assert is_map(result)
|
||||
assert Enum.sort([url1, url2]) == result |> Map.keys() |> Enum.sort()
|
||||
test "returns a list of only reachable elements",
|
||||
%{result: result, reachable_host: reachable_host} do
|
||||
assert is_list(result)
|
||||
assert [reachable_host] == result
|
||||
end
|
||||
|
||||
test "returns a map with `unreachable_since` values for keys",
|
||||
%{result: result, url1: url1, url2: url2} do
|
||||
assert is_map(result)
|
||||
assert %NaiveDateTime{} = result[url1]
|
||||
assert is_nil(result[url2])
|
||||
end
|
||||
|
||||
test "returns an empty map for empty list or list containing no hosts / url" do
|
||||
assert %{} == Instances.filter_reachable([])
|
||||
assert %{} == Instances.filter_reachable([nil])
|
||||
test "returns an empty list when provided no data" do
|
||||
assert [] == Instances.filter_reachable([])
|
||||
assert [] == Instances.filter_reachable([nil])
|
||||
end
|
||||
end
|
||||
|
||||
describe "set_reachable/1" do
|
||||
test "sets unreachable url or host reachable" do
|
||||
host = "domain.com"
|
||||
Instances.set_consistently_unreachable(host)
|
||||
Instances.set_unreachable(host)
|
||||
refute Instances.reachable?(host)
|
||||
|
||||
Instances.set_reachable(host)
|
||||
|
|
@ -103,22 +71,68 @@ defmodule Pleroma.InstancesTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "set_consistently_unreachable/1" do
|
||||
test "sets reachable url or host unreachable" do
|
||||
url = "http://domain.com?q="
|
||||
assert Instances.reachable?(url)
|
||||
describe "check_all_unreachable/0" do
|
||||
test "schedules ReachabilityWorker jobs for all unreachable instances" do
|
||||
domain1 = "unreachable1.example.com"
|
||||
domain2 = "unreachable2.example.com"
|
||||
domain3 = "unreachable3.example.com"
|
||||
|
||||
Instances.set_consistently_unreachable(url)
|
||||
refute Instances.reachable?(url)
|
||||
Instances.set_unreachable(domain1)
|
||||
Instances.set_unreachable(domain2)
|
||||
Instances.set_unreachable(domain3)
|
||||
|
||||
Instances.check_all_unreachable()
|
||||
|
||||
# Verify that ReachabilityWorker jobs were scheduled for all unreachable domains
|
||||
jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker)
|
||||
assert length(jobs) == 3
|
||||
|
||||
domains = Enum.map(jobs, & &1.args["domain"])
|
||||
assert domain1 in domains
|
||||
assert domain2 in domains
|
||||
assert domain3 in domains
|
||||
end
|
||||
|
||||
test "keeps unreachable url or host unreachable" do
|
||||
host = "site.name"
|
||||
Instances.set_consistently_unreachable(host)
|
||||
refute Instances.reachable?(host)
|
||||
test "does not schedule jobs for reachable instances" do
|
||||
unreachable_domain = "unreachable.example.com"
|
||||
reachable_domain = "reachable.example.com"
|
||||
|
||||
Instances.set_consistently_unreachable(host)
|
||||
refute Instances.reachable?(host)
|
||||
Instances.set_unreachable(unreachable_domain)
|
||||
Instances.set_reachable(reachable_domain)
|
||||
|
||||
Instances.check_all_unreachable()
|
||||
|
||||
# Verify that only one job was scheduled (for the unreachable domain)
|
||||
jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker)
|
||||
assert length(jobs) == 1
|
||||
[job] = jobs
|
||||
assert job.args["domain"] == unreachable_domain
|
||||
end
|
||||
end
|
||||
|
||||
test "delete_all_unreachable/0 schedules DeleteWorker jobs for all unreachable instances" do
|
||||
domain1 = "unreachable1.example.com"
|
||||
domain2 = "unreachable2.example.com"
|
||||
domain3 = "unreachable3.example.com"
|
||||
|
||||
Instances.set_unreachable(domain1)
|
||||
Instances.set_unreachable(domain2)
|
||||
Instances.set_unreachable(domain3)
|
||||
|
||||
Instances.delete_all_unreachable()
|
||||
|
||||
# Verify that DeleteWorker jobs were scheduled for all unreachable domains
|
||||
jobs = all_enqueued(worker: Pleroma.Workers.DeleteWorker)
|
||||
assert length(jobs) == 3
|
||||
|
||||
domains = Enum.map(jobs, & &1.args["host"])
|
||||
assert domain1 in domains
|
||||
assert domain2 in domains
|
||||
assert domain3 in domains
|
||||
|
||||
# Verify all jobs are delete_instance operations
|
||||
Enum.each(jobs, fn job ->
|
||||
assert job.args["op"] == "delete_instance"
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|
|||
|
||||
alias Pleroma.Integration.WebsocketClient
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
alias Pleroma.Web.OAuth
|
||||
|
||||
@moduletag needs_streamer: true, capture_log: true
|
||||
|
|
@ -31,6 +32,48 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|
|||
WebsocketClient.start_link(self(), path, headers)
|
||||
end
|
||||
|
||||
defp raw_websocket_handshake(qs, headers) do
|
||||
uri = URI.parse(@path <> qs)
|
||||
port = uri.port || 80
|
||||
path = uri.path <> if(uri.query, do: "?" <> uri.query, else: "")
|
||||
|
||||
default_headers = [
|
||||
{"host", "#{uri.host}:#{port}"},
|
||||
{"upgrade", "websocket"},
|
||||
{"connection", "Upgrade"},
|
||||
{"sec-websocket-key", Base.encode64(:crypto.strong_rand_bytes(16))},
|
||||
{"sec-websocket-version", "13"}
|
||||
]
|
||||
|
||||
request = [
|
||||
"GET #{path} HTTP/1.1\r\n",
|
||||
Enum.map(default_headers ++ headers, fn {name, value} -> "#{name}: #{value}\r\n" end),
|
||||
"\r\n"
|
||||
]
|
||||
|
||||
with {:ok, socket} <-
|
||||
:gen_tcp.connect(String.to_charlist(uri.host), port, [:binary, active: false], 1_000),
|
||||
:ok <- :gen_tcp.send(socket, request),
|
||||
{:ok, response} <- :gen_tcp.recv(socket, 0, 1_000) do
|
||||
:gen_tcp.close(socket)
|
||||
{:ok, parse_http_response(response)}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_http_response(response) do
|
||||
[headers | _] = String.split(response, "\r\n\r\n", parts: 2)
|
||||
[status_line | header_lines] = String.split(headers, "\r\n")
|
||||
[_, status | _] = String.split(status_line, " ")
|
||||
|
||||
headers =
|
||||
Enum.map(header_lines, fn line ->
|
||||
[name, value] = String.split(line, ":", parts: 2)
|
||||
{String.downcase(name), String.trim(value)}
|
||||
end)
|
||||
|
||||
%{status: String.to_integer(status), headers: headers}
|
||||
end
|
||||
|
||||
defp decode_json(json) do
|
||||
with {:ok, %{"event" => event, "payload" => payload_text}} <- Jason.decode(json),
|
||||
{:ok, payload} <- Jason.decode(payload_text) do
|
||||
|
|
@ -85,9 +128,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|
|||
assert json["payload"]
|
||||
assert {:ok, json} = Jason.decode(json["payload"])
|
||||
|
||||
view_json =
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil)
|
||||
|> atom_key_to_string()
|
||||
view_json = atom_key_to_string(StatusView.render("show.json", activity: activity, for: nil))
|
||||
|
||||
assert json == view_json
|
||||
end
|
||||
|
|
@ -114,10 +155,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|
|||
assert json["payload"]
|
||||
assert {:ok, json} = Jason.decode(json["payload"])
|
||||
|
||||
view_json =
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil)
|
||||
|> Jason.encode!()
|
||||
|> Jason.decode!()
|
||||
view_json = atom_key_to_string(StatusView.render("show.json", activity: activity, for: nil))
|
||||
|
||||
assert json == view_json
|
||||
end
|
||||
|
|
@ -268,6 +306,65 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "accepts valid token on Sec-WebSocket-Protocol header", %{token: token} do
|
||||
assert {:ok, _} = start_socket("?stream=user", [{"Sec-WebSocket-Protocol", token.token}])
|
||||
|
||||
capture_log(fn ->
|
||||
assert {:error, %WebSockex.RequestError{code: 401}} =
|
||||
start_socket("?stream=user", [{"Sec-WebSocket-Protocol", "I am a friend"}])
|
||||
|
||||
Process.sleep(30)
|
||||
end)
|
||||
end
|
||||
|
||||
test "echoes the Sec-WebSocket-Protocol token in the handshake", %{token: token} do
|
||||
assert {:ok, %{status: 101, headers: headers}} =
|
||||
raw_websocket_handshake("?stream=user", [
|
||||
{"sec-websocket-protocol", token.token}
|
||||
])
|
||||
|
||||
assert {"sec-websocket-protocol", token.token} in headers
|
||||
end
|
||||
|
||||
test "echoes the selected Sec-WebSocket-Protocol token", %{token: token} do
|
||||
assert {:ok, %{status: 101, headers: headers}} =
|
||||
raw_websocket_handshake("?stream=user", [
|
||||
{"sec-websocket-protocol", "#{token.token}, phoenix"}
|
||||
])
|
||||
|
||||
assert {"sec-websocket-protocol", token.token} in headers
|
||||
end
|
||||
|
||||
test "does not echo an invalid Sec-WebSocket-Protocol token", %{token: token} do
|
||||
assert {:ok, %{status: 401, headers: headers}} =
|
||||
raw_websocket_handshake("?stream=user", [
|
||||
{"sec-websocket-protocol", "invalid"}
|
||||
])
|
||||
|
||||
refute {"sec-websocket-protocol", token.token} in headers
|
||||
refute List.keymember?(headers, "sec-websocket-protocol", 0)
|
||||
end
|
||||
|
||||
test "prefers sec-websocket-protocol token over query access_token", %{
|
||||
token: token,
|
||||
user: user
|
||||
} do
|
||||
assert {:ok, state} =
|
||||
Pleroma.Web.MastodonAPI.WebsocketHandler.connect(%{
|
||||
params: %{"stream" => "user", "access_token" => "invalid"},
|
||||
connect_info: %{
|
||||
sec_websocket_headers: [
|
||||
{"sec-websocket-version", "13"},
|
||||
{"sec-websocket-protocol", token.token}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
assert state.user.id == user.id
|
||||
assert state.oauth_token.id == token.id
|
||||
assert state.topics != []
|
||||
end
|
||||
|
||||
test "accepts valid token on client-sent event", %{token: token} do
|
||||
assert {:ok, pid} = start_socket()
|
||||
|
||||
|
|
@ -352,7 +449,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|
|||
test "accepts the 'list' stream", %{token: token, user: user} do
|
||||
posting_user = insert(:user)
|
||||
|
||||
{:ok, list} = Pleroma.List.create("test", user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "test"}, user)
|
||||
Pleroma.List.follow(list, posting_user)
|
||||
|
||||
assert {:ok, _} = start_socket("?stream=list&access_token=#{token.token}&list=#{list.id}")
|
||||
|
|
@ -404,7 +501,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|
|||
|
||||
test "receives private statuses", %{user: reading_user, token: token} do
|
||||
user = insert(:user)
|
||||
CommonAPI.follow(reading_user, user)
|
||||
CommonAPI.follow(user, reading_user)
|
||||
|
||||
{:ok, _} = start_socket("?stream=user&access_token=#{token.token}")
|
||||
|
||||
|
|
@ -419,19 +516,19 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|
|||
assert {:ok, json} = Jason.decode(json["payload"])
|
||||
|
||||
view_json =
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("show.json",
|
||||
activity: activity,
|
||||
for: reading_user
|
||||
atom_key_to_string(
|
||||
StatusView.render("show.json",
|
||||
activity: activity,
|
||||
for: reading_user
|
||||
)
|
||||
)
|
||||
|> Jason.encode!()
|
||||
|> Jason.decode!()
|
||||
|
||||
assert json == view_json
|
||||
end
|
||||
|
||||
test "receives edits", %{user: reading_user, token: token} do
|
||||
user = insert(:user)
|
||||
CommonAPI.follow(reading_user, user)
|
||||
CommonAPI.follow(user, reading_user)
|
||||
|
||||
{:ok, _} = start_socket("?stream=user&access_token=#{token.token}")
|
||||
|
||||
|
|
@ -440,26 +537,26 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|
|||
|
||||
assert_receive {:text, _raw_json}, 1_000
|
||||
|
||||
{:ok, _} = CommonAPI.update(user, activity, %{status: "mew mew", visibility: "private"})
|
||||
{:ok, _} = CommonAPI.update(activity, user, %{status: "mew mew", visibility: "private"})
|
||||
|
||||
assert_receive {:text, raw_json}, 1_000
|
||||
|
||||
activity = Pleroma.Activity.normalize(activity)
|
||||
|
||||
view_json =
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("show.json",
|
||||
activity: activity,
|
||||
for: reading_user
|
||||
atom_key_to_string(
|
||||
StatusView.render("show.json",
|
||||
activity: activity,
|
||||
for: reading_user
|
||||
)
|
||||
)
|
||||
|> Jason.encode!()
|
||||
|> Jason.decode!()
|
||||
|
||||
assert {:ok, %{"event" => "status.update", "payload" => ^view_json}} = decode_json(raw_json)
|
||||
end
|
||||
|
||||
test "receives notifications", %{user: reading_user, token: token} do
|
||||
user = insert(:user)
|
||||
CommonAPI.follow(reading_user, user)
|
||||
CommonAPI.follow(user, reading_user)
|
||||
|
||||
{:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}")
|
||||
|
||||
|
|
|
|||
|
|
@ -28,13 +28,13 @@ defmodule Pleroma.JobQueueMonitorTest do
|
|||
queue: "background",
|
||||
stack: [
|
||||
{Pleroma.Workers.BackgroundWorker, :perform, 2,
|
||||
[file: 'lib/pleroma/workers/background_worker.ex', line: 31]},
|
||||
[file: ~c"lib/pleroma/workers/background_worker.ex", line: 31]},
|
||||
{Oban.Queue.Executor, :safe_call, 1,
|
||||
[file: 'lib/oban/queue/executor.ex', line: 42]},
|
||||
{:timer, :tc, 3, [file: 'timer.erl', line: 197]},
|
||||
{Oban.Queue.Executor, :call, 2, [file: 'lib/oban/queue/executor.ex', line: 23]},
|
||||
{Task.Supervised, :invoke_mfa, 2, [file: 'lib/task/supervised.ex', line: 90]},
|
||||
{:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 249]}
|
||||
[file: ~c"lib/oban/queue/executor.ex", line: 42]},
|
||||
{:timer, :tc, 3, [file: ~c"timer.erl", line: 197]},
|
||||
{Oban.Queue.Executor, :call, 2, [file: ~c"lib/oban/queue/executor.ex", line: 23]},
|
||||
{Task.Supervised, :invoke_mfa, 2, [file: ~c"lib/task/supervised.ex", line: 90]},
|
||||
{:proc_lib, :init_p_do_apply, 3, [file: ~c"proc_lib.erl", line: 249]}
|
||||
],
|
||||
worker: "Pleroma.Workers.BackgroundWorker"
|
||||
}}
|
||||
|
|
|
|||
56
test/pleroma/language/language_detector_test.exs
Normal file
56
test/pleroma/language/language_detector_test.exs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Language.LanguageDetectorTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.Language.LanguageDetector
|
||||
alias Pleroma.Language.LanguageDetectorMock
|
||||
alias Pleroma.StaticStubbedConfigMock
|
||||
|
||||
import Mox
|
||||
|
||||
setup do
|
||||
# Stub the StaticStubbedConfigMock to return our mock for the provider
|
||||
StaticStubbedConfigMock
|
||||
|> stub(:get, fn
|
||||
[Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock
|
||||
_other -> nil
|
||||
end)
|
||||
|
||||
# Stub the LanguageDetectorMock with default implementations
|
||||
LanguageDetectorMock
|
||||
|> stub(:missing_dependencies, fn -> [] end)
|
||||
|> stub(:configured?, fn -> true end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "it detects text language" do
|
||||
LanguageDetectorMock
|
||||
|> expect(:detect, fn _text -> "fr" end)
|
||||
|
||||
detected_language = LanguageDetector.detect("Je viens d'atterrir en Tchéquie.")
|
||||
|
||||
assert detected_language == "fr"
|
||||
end
|
||||
|
||||
test "it returns nil if text is not long enough" do
|
||||
# No need to set expectations as the word count check happens before the provider is called
|
||||
|
||||
detected_language = LanguageDetector.detect("it returns nil")
|
||||
|
||||
assert detected_language == nil
|
||||
end
|
||||
|
||||
test "it returns nil if no provider specified" do
|
||||
# Override the stub to return nil for the provider
|
||||
StaticStubbedConfigMock
|
||||
|> expect(:get, fn [Pleroma.Language.LanguageDetector, :provider] -> nil end)
|
||||
|
||||
detected_language = LanguageDetector.detect("this should also return nil")
|
||||
|
||||
assert detected_language == nil
|
||||
end
|
||||
end
|
||||
37
test/pleroma/language/translation/deepl_test.exs
Normal file
37
test/pleroma/language/translation/deepl_test.exs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Language.Translation.DeeplTest do
|
||||
use Pleroma.Web.ConnCase
|
||||
|
||||
alias Pleroma.Language.Translation.Deepl
|
||||
|
||||
test "it translates text" do
|
||||
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||
clear_config([Pleroma.Language.Translation.Deepl, :base_url], "https://api-free.deepl.com")
|
||||
clear_config([Pleroma.Language.Translation.Deepl, :api_key], "API_KEY")
|
||||
|
||||
{:ok, res} =
|
||||
Deepl.translate(
|
||||
"USUNĄĆ ŚLEDZIKA!Wklej to na swojego śledzika. Jeżeli uzbieramy 70% użytkowników nk...to usuną śledzika!!!",
|
||||
"pl",
|
||||
"en"
|
||||
)
|
||||
|
||||
assert %{
|
||||
detected_source_language: "PL",
|
||||
provider: "DeepL"
|
||||
} = res
|
||||
end
|
||||
|
||||
test "it returns languages list" do
|
||||
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||
clear_config([Pleroma.Language.Translation.Deepl, :base_url], "https://api-free.deepl.com")
|
||||
clear_config([Pleroma.Language.Translation.Deepl, :api_key], "API_KEY")
|
||||
|
||||
assert {:ok, [language | _languages]} = Deepl.supported_languages(:target)
|
||||
|
||||
assert is_binary(language)
|
||||
end
|
||||
end
|
||||
59
test/pleroma/language/translation/translate_locally_test.exs
Normal file
59
test/pleroma/language/translation/translate_locally_test.exs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Language.Translation.TranslateLocallyTest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
alias Pleroma.Language.Translation.TranslateLocally
|
||||
|
||||
@example_models %{
|
||||
"de" => %{
|
||||
"en" => "de-en-base"
|
||||
},
|
||||
"en" => %{
|
||||
"de" => "en-de-base",
|
||||
"pl" => "en-pl-tiny"
|
||||
},
|
||||
"cs" => %{
|
||||
"en" => "cs-en-base"
|
||||
},
|
||||
"pl" => %{
|
||||
"en" => "pl-en-tiny"
|
||||
}
|
||||
}
|
||||
|
||||
test "it returns languages list" do
|
||||
clear_config([Pleroma.Language.Translation.TranslateLocally, :models], @example_models)
|
||||
|
||||
assert {:ok, languages} = TranslateLocally.supported_languages(:source)
|
||||
assert ["cs", "de", "en", "pl"] = languages |> Enum.sort()
|
||||
end
|
||||
|
||||
describe "it returns languages matrix" do
|
||||
test "without intermediary language" do
|
||||
clear_config([Pleroma.Language.Translation.TranslateLocally, :models], @example_models)
|
||||
|
||||
assert {:ok,
|
||||
%{
|
||||
"cs" => ["en"],
|
||||
"de" => ["en"],
|
||||
"en" => ["de", "pl"],
|
||||
"pl" => ["en"]
|
||||
}} = TranslateLocally.languages_matrix()
|
||||
end
|
||||
|
||||
test "with intermediary language" do
|
||||
clear_config([Pleroma.Language.Translation.TranslateLocally, :models], @example_models)
|
||||
clear_config([Pleroma.Language.Translation.TranslateLocally, :intermediary_language], "en")
|
||||
|
||||
assert {:ok,
|
||||
%{
|
||||
"cs" => ["de", "en", "pl"],
|
||||
"de" => ["en", "pl"],
|
||||
"en" => ["de", "pl"],
|
||||
"pl" => ["de", "en"]
|
||||
}} = TranslateLocally.languages_matrix()
|
||||
end
|
||||
end
|
||||
end
|
||||
28
test/pleroma/language/translation_test.exs
Normal file
28
test/pleroma/language/translation_test.exs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
defmodule Pleroma.Language.TranslationTest do
|
||||
use Pleroma.Web.ConnCase
|
||||
|
||||
alias Pleroma.Language.Translation
|
||||
|
||||
setup do: clear_config([Pleroma.Language.Translation, :provider], TranslationMock)
|
||||
|
||||
test "it translates text" do
|
||||
assert {:ok,
|
||||
%{
|
||||
content: "txet emos",
|
||||
detected_source_language: _,
|
||||
provider: _
|
||||
}} = Translation.translate("some text", "en", "uk")
|
||||
end
|
||||
|
||||
test "it stores translation result in cache" do
|
||||
Translation.translate("some text", "en", "uk")
|
||||
|
||||
assert {:ok, result} =
|
||||
Cachex.get(
|
||||
:translations_cache,
|
||||
"en/uk/#{:crypto.hash(:sha256, "some text") |> Base.encode64()}"
|
||||
)
|
||||
|
||||
assert result.content == "txet emos"
|
||||
end
|
||||
end
|
||||
|
|
@ -10,22 +10,46 @@ defmodule Pleroma.ListTest do
|
|||
|
||||
test "creating a list" do
|
||||
user = insert(:user)
|
||||
{:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user)
|
||||
%Pleroma.List{title: title} = Pleroma.List.get(list.id, user)
|
||||
{:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title"}, user)
|
||||
%Pleroma.List{title: title, exclusive: exclusive} = Pleroma.List.get(list.id, user)
|
||||
assert title == "title"
|
||||
assert exclusive == false
|
||||
end
|
||||
|
||||
test "validates title" do
|
||||
user = insert(:user)
|
||||
|
||||
assert {:error, changeset} = Pleroma.List.create("", user)
|
||||
assert {:error, changeset} = Pleroma.List.create(%{title: ""}, user)
|
||||
assert changeset.errors == [title: {"can't be blank", [validation: :required]}]
|
||||
end
|
||||
|
||||
test "creating a list with an emoji" do
|
||||
user = insert(:user)
|
||||
{:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title", emoji: "🕓"}, user)
|
||||
%Pleroma.List{emoji: emoji} = Pleroma.List.get(list.id, user)
|
||||
assert emoji == "🕓"
|
||||
end
|
||||
|
||||
test "validates emoji" do
|
||||
user = insert(:user)
|
||||
|
||||
assert {:error, changeset} =
|
||||
Pleroma.List.create(%{title: "title", emoji: "not an emoji"}, user)
|
||||
|
||||
assert changeset.errors == [emoji: {"Invalid emoji", []}]
|
||||
end
|
||||
|
||||
test "updating a list's emoji" do
|
||||
user = insert(:user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "title"}, user)
|
||||
{:ok, %{emoji: emoji}} = Pleroma.List.update(list, %{emoji: "🕓"})
|
||||
assert "🕓" == emoji
|
||||
end
|
||||
|
||||
test "getting a list not belonging to the user" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
{:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user)
|
||||
{:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title"}, user)
|
||||
ret = Pleroma.List.get(list.id, other_user)
|
||||
assert is_nil(ret)
|
||||
end
|
||||
|
|
@ -33,7 +57,7 @@ defmodule Pleroma.ListTest do
|
|||
test "adding an user to a list" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
{:ok, list} = Pleroma.List.create("title", user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "title"}, user)
|
||||
{:ok, %{following: following}} = Pleroma.List.follow(list, other_user)
|
||||
assert [other_user.follower_address] == following
|
||||
end
|
||||
|
|
@ -41,7 +65,7 @@ defmodule Pleroma.ListTest do
|
|||
test "removing an user from a list" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
{:ok, list} = Pleroma.List.create("title", user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "title"}, user)
|
||||
{:ok, %{following: _following}} = Pleroma.List.follow(list, other_user)
|
||||
{:ok, %{following: following}} = Pleroma.List.unfollow(list, other_user)
|
||||
assert [] == following
|
||||
|
|
@ -49,14 +73,27 @@ defmodule Pleroma.ListTest do
|
|||
|
||||
test "renaming a list" do
|
||||
user = insert(:user)
|
||||
{:ok, list} = Pleroma.List.create("title", user)
|
||||
{:ok, %{title: title}} = Pleroma.List.rename(list, "new")
|
||||
{:ok, list} = Pleroma.List.create(%{title: "title"}, user)
|
||||
{:ok, %{title: title}} = Pleroma.List.update(list, %{title: "new"})
|
||||
assert "new" == title
|
||||
end
|
||||
|
||||
test "updating a list exclusivity" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, %{exclusive: exclusive} = list} =
|
||||
Pleroma.List.create(%{title: "title", exclusive: true}, user)
|
||||
|
||||
assert exclusive == true
|
||||
{:ok, %{exclusive: exclusive} = list} = Pleroma.List.update(list, %{exclusive: false})
|
||||
assert exclusive == false
|
||||
{:ok, %{exclusive: exclusive}} = Pleroma.List.update(list, %{exclusive: true})
|
||||
assert exclusive == true
|
||||
end
|
||||
|
||||
test "deleting a list" do
|
||||
user = insert(:user)
|
||||
{:ok, list} = Pleroma.List.create("title", user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "title"}, user)
|
||||
{:ok, list} = Pleroma.List.delete(list)
|
||||
assert is_nil(Repo.get(Pleroma.List, list.id))
|
||||
end
|
||||
|
|
@ -65,7 +102,7 @@ defmodule Pleroma.ListTest do
|
|||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
third_user = insert(:user)
|
||||
{:ok, list} = Pleroma.List.create("title", user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "title"}, user)
|
||||
{:ok, list} = Pleroma.List.follow(list, other_user)
|
||||
{:ok, list} = Pleroma.List.follow(list, third_user)
|
||||
{:ok, following} = Pleroma.List.get_following(list)
|
||||
|
|
@ -76,9 +113,9 @@ defmodule Pleroma.ListTest do
|
|||
test "getting all lists by an user" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
{:ok, list_one} = Pleroma.List.create("title", user)
|
||||
{:ok, list_two} = Pleroma.List.create("other title", user)
|
||||
{:ok, list_three} = Pleroma.List.create("third title", other_user)
|
||||
{:ok, list_one} = Pleroma.List.create(%{title: "title"}, user)
|
||||
{:ok, list_two} = Pleroma.List.create(%{title: "other title"}, user)
|
||||
{:ok, list_three} = Pleroma.List.create(%{title: "third title"}, other_user)
|
||||
lists = Pleroma.List.for_user(user, %{})
|
||||
assert list_one in lists
|
||||
assert list_two in lists
|
||||
|
|
@ -88,9 +125,9 @@ defmodule Pleroma.ListTest do
|
|||
test "getting all lists the user is a member of" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
{:ok, list_one} = Pleroma.List.create("title", user)
|
||||
{:ok, list_two} = Pleroma.List.create("other title", user)
|
||||
{:ok, list_three} = Pleroma.List.create("third title", other_user)
|
||||
{:ok, list_one} = Pleroma.List.create(%{title: "title"}, user)
|
||||
{:ok, list_two} = Pleroma.List.create(%{title: "other title"}, user)
|
||||
{:ok, list_three} = Pleroma.List.create(%{title: "third title"}, other_user)
|
||||
{:ok, list_one} = Pleroma.List.follow(list_one, other_user)
|
||||
{:ok, list_two} = Pleroma.List.follow(list_two, other_user)
|
||||
{:ok, list_three} = Pleroma.List.follow(list_three, user)
|
||||
|
|
@ -106,8 +143,8 @@ defmodule Pleroma.ListTest do
|
|||
not_owner = insert(:user)
|
||||
member_1 = insert(:user)
|
||||
member_2 = insert(:user)
|
||||
{:ok, owned_list} = Pleroma.List.create("owned", owner)
|
||||
{:ok, not_owned_list} = Pleroma.List.create("not owned", not_owner)
|
||||
{:ok, owned_list} = Pleroma.List.create(%{title: "owned"}, owner)
|
||||
{:ok, not_owned_list} = Pleroma.List.create(%{title: "not owned"}, not_owner)
|
||||
{:ok, owned_list} = Pleroma.List.follow(owned_list, member_1)
|
||||
{:ok, owned_list} = Pleroma.List.follow(owned_list, member_2)
|
||||
{:ok, not_owned_list} = Pleroma.List.follow(not_owned_list, member_1)
|
||||
|
|
@ -123,14 +160,14 @@ defmodule Pleroma.ListTest do
|
|||
|
||||
test "get by ap_id" do
|
||||
user = insert(:user)
|
||||
{:ok, list} = Pleroma.List.create("foo", user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "foo"}, user)
|
||||
assert Pleroma.List.get_by_ap_id(list.ap_id) == list
|
||||
end
|
||||
|
||||
test "memberships" do
|
||||
user = insert(:user)
|
||||
member = insert(:user)
|
||||
{:ok, list} = Pleroma.List.create("foo", user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "foo"}, user)
|
||||
{:ok, list} = Pleroma.List.follow(list, member)
|
||||
|
||||
assert Pleroma.List.memberships(member) == [list.ap_id]
|
||||
|
|
@ -140,7 +177,7 @@ defmodule Pleroma.ListTest do
|
|||
user = insert(:user)
|
||||
member = insert(:user)
|
||||
|
||||
{:ok, list} = Pleroma.List.create("foo", user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "foo"}, user)
|
||||
{:ok, list} = Pleroma.List.follow(list, member)
|
||||
|
||||
assert Pleroma.List.member?(list, member)
|
||||
|
|
|
|||
|
|
@ -36,11 +36,12 @@ defmodule Pleroma.MarkerTest do
|
|||
insert(:notification, user: user, activity: insert(:note_activity))
|
||||
insert(:notification, user: user, activity: insert(:note_activity))
|
||||
insert(:marker, timeline: "home", user: user)
|
||||
%Marker{} = refreshed_marker = refresh_record(marker)
|
||||
|
||||
assert Marker.get_markers(
|
||||
user,
|
||||
["notifications"]
|
||||
) == [%Marker{refresh_record(marker) | unread_count: 2}]
|
||||
) == [%{refreshed_marker | unread_count: 2}]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ defmodule Pleroma.MigrationHelper.NotificationBackfillTest do
|
|||
{:ok, post} = CommonAPI.post(user, %{status: "yeah, @#{other_user.nickname}"})
|
||||
{:ok, chat} = CommonAPI.post_chat_message(user, other_user, "yo")
|
||||
{:ok, react} = CommonAPI.react_with_emoji(post.id, other_user, "☕")
|
||||
{:ok, like} = CommonAPI.favorite(other_user, post.id)
|
||||
{:ok, like} = CommonAPI.favorite(post.id, other_user)
|
||||
{:ok, react_2} = CommonAPI.react_with_emoji(post.id, other_user, "☕")
|
||||
|
||||
data =
|
||||
|
|
|
|||
|
|
@ -308,4 +308,37 @@ defmodule Pleroma.ModerationLogTest do
|
|||
assert log.data["message"] == "@#{moderator.nickname} deleted status ##{note.id}"
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_log_entry_message/1" do
|
||||
setup do
|
||||
moderator = insert(:user, is_moderator: true)
|
||||
[moderator: moderator]
|
||||
end
|
||||
|
||||
test "handles unknown action types gracefully", %{moderator: moderator} do
|
||||
log_entry = %ModerationLog{
|
||||
data: %{
|
||||
"actor" => %{"nickname" => moderator.nickname},
|
||||
"action" => "unknown_action",
|
||||
"some_data" => "test_value"
|
||||
}
|
||||
}
|
||||
|
||||
assert ModerationLog.get_log_entry_message(log_entry) =~ moderator.nickname
|
||||
assert ModerationLog.get_log_entry_message(log_entry) =~ "unknown_action"
|
||||
end
|
||||
|
||||
test "handles malformed log entries gracefully" do
|
||||
log_entry = %ModerationLog{
|
||||
data: %{
|
||||
"action" => "force_password_reset"
|
||||
# Missing "actor" and "subject" fields
|
||||
}
|
||||
}
|
||||
|
||||
message = ModerationLog.get_log_entry_message(log_entry)
|
||||
assert is_binary(message)
|
||||
assert message =~ "force_password_reset"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -17,9 +17,10 @@ defmodule Pleroma.NotificationTest do
|
|||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.MastodonAPI.NotificationView
|
||||
alias Pleroma.Web.Streamer
|
||||
|
||||
setup do
|
||||
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
|
||||
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
|
||||
:ok
|
||||
end
|
||||
|
||||
|
|
@ -165,7 +166,7 @@ defmodule Pleroma.NotificationTest do
|
|||
{:ok, _activity_two} = CommonAPI.repeat(activity_one.id, repeated_user)
|
||||
|
||||
{:ok, _edit_activity} =
|
||||
CommonAPI.update(user, activity_one, %{
|
||||
CommonAPI.update(activity_one, user, %{
|
||||
status: "hey @#{other_user.nickname}! mew mew"
|
||||
})
|
||||
|
||||
|
|
@ -180,8 +181,8 @@ defmodule Pleroma.NotificationTest do
|
|||
question = insert(:question, user: user1)
|
||||
activity = insert(:question_activity, question: question)
|
||||
|
||||
{:ok, _, _} = CommonAPI.vote(user2, question, [0])
|
||||
{:ok, _, _} = CommonAPI.vote(user3, question, [1])
|
||||
{:ok, _, _} = CommonAPI.vote(question, user2, [0])
|
||||
{:ok, _, _} = CommonAPI.vote(question, user3, [1])
|
||||
|
||||
{:ok, notifications} = Notification.create_poll_notifications(activity)
|
||||
|
||||
|
|
@ -209,7 +210,7 @@ defmodule Pleroma.NotificationTest do
|
|||
notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true}
|
||||
)
|
||||
|
||||
CommonAPI.follow(follower, followed)
|
||||
CommonAPI.follow(followed, follower)
|
||||
{:ok, activity} = CommonAPI.post(follower, %{status: "hey @#{followed.nickname}"})
|
||||
refute Notification.create_notification(activity, followed)
|
||||
end
|
||||
|
|
@ -222,7 +223,7 @@ defmodule Pleroma.NotificationTest do
|
|||
notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true}
|
||||
)
|
||||
|
||||
CommonAPI.follow(receiver, poster)
|
||||
CommonAPI.follow(poster, receiver)
|
||||
{:ok, activity} = CommonAPI.post(poster, %{status: "hey @#{receiver.nickname}"})
|
||||
assert Notification.create_notification(activity, receiver)
|
||||
end
|
||||
|
|
@ -238,7 +239,7 @@ defmodule Pleroma.NotificationTest do
|
|||
user = insert(:user)
|
||||
subscriber = insert(:user)
|
||||
|
||||
{:ok, _, _, _} = CommonAPI.follow(subscriber, user)
|
||||
{:ok, _, _, _} = CommonAPI.follow(user, subscriber)
|
||||
User.subscribe(subscriber, user)
|
||||
{:ok, status} = CommonAPI.post(user, %{status: "Akariiiin"})
|
||||
{:ok, [_notif]} = Notification.create_notifications(status)
|
||||
|
|
@ -295,7 +296,7 @@ defmodule Pleroma.NotificationTest do
|
|||
insert(:filter, user: user, phrase: "tesla", hide: true)
|
||||
|
||||
{:ok, activity_one} = CommonAPI.post(user, %{status: "wow tesla"})
|
||||
{:ok, activity_two} = CommonAPI.favorite(other_user, activity_one.id)
|
||||
{:ok, activity_two} = CommonAPI.favorite(activity_one.id, other_user)
|
||||
|
||||
{:ok, [notification]} = Notification.create_notifications(activity_two)
|
||||
|
||||
|
|
@ -309,7 +310,7 @@ defmodule Pleroma.NotificationTest do
|
|||
user = insert(:user)
|
||||
followed_user = insert(:user, is_locked: false)
|
||||
|
||||
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
|
||||
{:ok, _, _, _activity} = CommonAPI.follow(followed_user, user)
|
||||
assert FollowingRelationship.following?(user, followed_user)
|
||||
assert [notification] = Notification.for_user(followed_user)
|
||||
|
||||
|
|
@ -324,7 +325,7 @@ defmodule Pleroma.NotificationTest do
|
|||
user = insert(:user)
|
||||
followed_user = insert(:user, is_locked: true)
|
||||
|
||||
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
|
||||
{:ok, _, _, _activity} = CommonAPI.follow(followed_user, user)
|
||||
refute FollowingRelationship.following?(user, followed_user)
|
||||
assert [notification] = Notification.for_user(followed_user)
|
||||
|
||||
|
|
@ -339,19 +340,22 @@ defmodule Pleroma.NotificationTest do
|
|||
|> Repo.preload(:activity)
|
||||
|
||||
assert %{type: "follow"} =
|
||||
NotificationView.render("show.json", notification: notification, for: followed_user)
|
||||
NotificationView.render("show.json",
|
||||
notification: notification,
|
||||
for: followed_user
|
||||
)
|
||||
end
|
||||
|
||||
test "it doesn't create a notification for follow-unfollow-follow chains" do
|
||||
user = insert(:user)
|
||||
followed_user = insert(:user, is_locked: false)
|
||||
|
||||
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
|
||||
{:ok, _, _, _activity} = CommonAPI.follow(followed_user, user)
|
||||
assert FollowingRelationship.following?(user, followed_user)
|
||||
assert [notification] = Notification.for_user(followed_user)
|
||||
|
||||
CommonAPI.unfollow(user, followed_user)
|
||||
{:ok, _, _, _activity_dupe} = CommonAPI.follow(user, followed_user)
|
||||
CommonAPI.unfollow(followed_user, user)
|
||||
{:ok, _, _, _activity_dupe} = CommonAPI.follow(followed_user, user)
|
||||
|
||||
notification_id = notification.id
|
||||
assert [%{id: ^notification_id}] = Notification.for_user(followed_user)
|
||||
|
|
@ -360,7 +364,7 @@ defmodule Pleroma.NotificationTest do
|
|||
test "dismisses the notification on follow request rejection" do
|
||||
user = insert(:user, is_locked: true)
|
||||
follower = insert(:user)
|
||||
{:ok, _, _, _follow_activity} = CommonAPI.follow(follower, user)
|
||||
{:ok, _, _, _follow_activity} = CommonAPI.follow(user, follower)
|
||||
assert [_notification] = Notification.for_user(user)
|
||||
{:ok, _follower} = CommonAPI.reject_follow_request(follower, user)
|
||||
assert [] = Notification.for_user(user)
|
||||
|
|
@ -443,8 +447,7 @@ defmodule Pleroma.NotificationTest do
|
|||
|
||||
describe "set_read_up_to()" do
|
||||
test "it sets all notifications as read up to a specified notification ID" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
[user, other_user] = insert_pair(:user)
|
||||
|
||||
{:ok, _activity} =
|
||||
CommonAPI.post(user, %{
|
||||
|
|
@ -483,6 +486,37 @@ defmodule Pleroma.NotificationTest do
|
|||
|
||||
assert m.last_read_id == to_string(n2.id)
|
||||
end
|
||||
|
||||
@tag needs_streamer: true
|
||||
test "it sends updated marker to the 'user' and the 'user:notification' stream" do
|
||||
%{user: user, token: oauth_token} = oauth_access(["read"])
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, _activity} =
|
||||
CommonAPI.post(other_user, %{
|
||||
status: "hi @#{user.nickname}!"
|
||||
})
|
||||
|
||||
[%{id: notification_id}] = Notification.for_user(user)
|
||||
|
||||
notification_id = to_string(notification_id)
|
||||
|
||||
task =
|
||||
Task.async(fn ->
|
||||
{:ok, _topic} =
|
||||
Streamer.get_topic_and_add_socket("user:notification", user, oauth_token)
|
||||
|
||||
assert_receive {:text, event}, 4_000
|
||||
|
||||
assert %{"event" => "marker", "payload" => payload} = Jason.decode!(event)
|
||||
|
||||
assert %{"notifications" => %{"last_read_id" => ^notification_id}} =
|
||||
Jason.decode!(payload)
|
||||
end)
|
||||
|
||||
Notification.set_read_up_to(user, notification_id)
|
||||
Task.await(task)
|
||||
end
|
||||
end
|
||||
|
||||
describe "for_user_since/2" do
|
||||
|
|
@ -614,7 +648,7 @@ defmodule Pleroma.NotificationTest do
|
|||
status: "hey @#{other_user.nickname}!"
|
||||
})
|
||||
|
||||
{:ok, activity_two} = CommonAPI.favorite(third_user, activity_one.id)
|
||||
{:ok, activity_two} = CommonAPI.favorite(activity_one.id, third_user)
|
||||
|
||||
enabled_receivers = Notification.get_notified_from_activity(activity_two)
|
||||
|
||||
|
|
@ -690,7 +724,7 @@ defmodule Pleroma.NotificationTest do
|
|||
|
||||
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"})
|
||||
|
||||
{:ok, _} = CommonAPI.add_mute(other_user, activity)
|
||||
{:ok, _} = CommonAPI.add_mute(activity, other_user)
|
||||
|
||||
{:ok, same_context_activity} =
|
||||
CommonAPI.post(user, %{
|
||||
|
|
@ -745,7 +779,7 @@ defmodule Pleroma.NotificationTest do
|
|||
{:ok, _activity_two} = CommonAPI.repeat(activity_one.id, repeated_user)
|
||||
|
||||
{:ok, edit_activity} =
|
||||
CommonAPI.update(user, activity_one, %{
|
||||
CommonAPI.update(activity_one, user, %{
|
||||
status: "hey @#{other_user.nickname}! mew mew"
|
||||
})
|
||||
|
||||
|
|
@ -765,7 +799,7 @@ defmodule Pleroma.NotificationTest do
|
|||
|
||||
assert Enum.empty?(Notification.for_user(user))
|
||||
|
||||
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
|
||||
{:ok, _} = CommonAPI.favorite(activity.id, other_user)
|
||||
|
||||
assert length(Notification.for_user(user)) == 1
|
||||
|
||||
|
|
@ -782,7 +816,7 @@ defmodule Pleroma.NotificationTest do
|
|||
|
||||
assert Enum.empty?(Notification.for_user(user))
|
||||
|
||||
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
|
||||
{:ok, _} = CommonAPI.favorite(activity.id, other_user)
|
||||
|
||||
assert length(Notification.for_user(user)) == 1
|
||||
|
||||
|
|
@ -837,7 +871,7 @@ defmodule Pleroma.NotificationTest do
|
|||
|
||||
assert Enum.empty?(Notification.for_user(user))
|
||||
|
||||
{:error, :not_found} = CommonAPI.favorite(other_user, activity.id)
|
||||
{:error, :not_found} = CommonAPI.favorite(activity.id, other_user)
|
||||
|
||||
assert Enum.empty?(Notification.for_user(user))
|
||||
end
|
||||
|
|
@ -1087,7 +1121,7 @@ defmodule Pleroma.NotificationTest do
|
|||
another_user = insert(:user)
|
||||
|
||||
{:ok, activity} = CommonAPI.post(user, %{status: "Give me my cofe!"})
|
||||
{:ok, _} = CommonAPI.favorite(another_user, activity.id)
|
||||
{:ok, _} = CommonAPI.favorite(activity.id, another_user)
|
||||
|
||||
assert length(Notification.for_user(user)) == 1
|
||||
end
|
||||
|
|
@ -1098,7 +1132,7 @@ defmodule Pleroma.NotificationTest do
|
|||
|
||||
insert(:filter, user: followed_user, phrase: "test", hide: true)
|
||||
|
||||
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
|
||||
{:ok, _, _, _activity} = CommonAPI.follow(followed_user, user)
|
||||
refute FollowingRelationship.following?(user, followed_user)
|
||||
assert [notification] = Notification.for_user(followed_user)
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ defmodule Pleroma.Object.ContainmentTest do
|
|||
})
|
||||
end
|
||||
|
||||
test "rejects an empty actor list without crashing" do
|
||||
assert :error ==
|
||||
Containment.contain_origin("https://example.com/objects/1", %{"actor" => []})
|
||||
end
|
||||
|
||||
test "contain_origin_from_id() catches obvious spoofing attempts" do
|
||||
data = %{
|
||||
"id" => "http://example.com/~alyssa/activities/1234.json"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ defmodule Pleroma.Object.FetcherTest do
|
|||
use Pleroma.DataCase
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Instances
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Object.Fetcher
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidator
|
||||
|
|
@ -84,7 +83,6 @@ defmodule Pleroma.Object.FetcherTest do
|
|||
:ok
|
||||
end
|
||||
|
||||
@tag capture_log: true
|
||||
test "it works when fetching the OP actor errors out" do
|
||||
# Here we simulate a case where the author of the OP can't be read
|
||||
assert {:ok, _} =
|
||||
|
|
@ -101,7 +99,7 @@ defmodule Pleroma.Object.FetcherTest do
|
|||
test "it returns thread depth exceeded error if thread depth is exceeded" do
|
||||
clear_config([:instance, :federation_incoming_replies_max_depth], 0)
|
||||
|
||||
assert {:error, :allowed_depth} = Fetcher.fetch_object_from_id(@ap_id, depth: 1)
|
||||
assert {:allowed_depth, false} = Fetcher.fetch_object_from_id(@ap_id, depth: 1)
|
||||
end
|
||||
|
||||
test "it fetches object if max thread depth is restricted to 0 and depth is not specified" do
|
||||
|
|
@ -119,15 +117,18 @@ defmodule Pleroma.Object.FetcherTest do
|
|||
|
||||
describe "actor origin containment" do
|
||||
test "it rejects objects with a bogus origin" do
|
||||
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json")
|
||||
{:containment, :error} =
|
||||
Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json")
|
||||
end
|
||||
|
||||
test "it rejects objects when attributedTo is wrong (variant 1)" do
|
||||
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json")
|
||||
{:containment, :error} =
|
||||
Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json")
|
||||
end
|
||||
|
||||
test "it rejects objects when attributedTo is wrong (variant 2)" do
|
||||
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity3.json")
|
||||
{:containment, :error} =
|
||||
Fetcher.fetch_object_from_id("https://info.pleroma.site/activity3.json")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -151,28 +152,102 @@ defmodule Pleroma.Object.FetcherTest do
|
|||
clear_config([:mrf_keyword, :reject], ["yeah"])
|
||||
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
|
||||
|
||||
assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} ==
|
||||
assert {:transmogrifier, {:reject, "[KeywordPolicy] Matches with rejected keyword"}} ==
|
||||
Fetcher.fetch_object_from_id(
|
||||
"http://mastodon.example.org/@admin/99541947525187367"
|
||||
)
|
||||
end
|
||||
|
||||
test "it does not fetch a spoofed object uploaded on an instance as an attachment" do
|
||||
assert {:error, _} =
|
||||
assert {:fetch, {:error, {:content_type, "application/json"}}} =
|
||||
Fetcher.fetch_object_from_id(
|
||||
"https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json"
|
||||
)
|
||||
end
|
||||
|
||||
test "it resets instance reachability on successful fetch" do
|
||||
id = "http://mastodon.example.org/@admin/99541947525187367"
|
||||
Instances.set_consistently_unreachable(id)
|
||||
refute Instances.reachable?(id)
|
||||
test "it does not fetch from local instance" do
|
||||
local_url = Pleroma.Web.Endpoint.url() <> "/objects/local_resource"
|
||||
|
||||
{:ok, _object} =
|
||||
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
|
||||
assert {:fetch, {:error, "Trying to fetch local resource"}} =
|
||||
Fetcher.fetch_object_from_id(local_url)
|
||||
end
|
||||
|
||||
assert Instances.reachable?(id)
|
||||
test "it validates content-type headers according to ActivityPub spec" do
|
||||
# Setup a mock for an object with invalid content-type
|
||||
mock(fn
|
||||
%{method: :get, url: "https://example.com/objects/invalid-content-type"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
# Not a valid AP content-type
|
||||
headers: [{"content-type", "application/json"}],
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
"id" => "https://example.com/objects/invalid-content-type",
|
||||
"type" => "Note",
|
||||
"content" => "This has an invalid content type",
|
||||
"actor" => "https://example.com/users/actor",
|
||||
"attributedTo" => "https://example.com/users/actor"
|
||||
})
|
||||
}
|
||||
end)
|
||||
|
||||
assert {:fetch, {:error, {:content_type, "application/json"}}} =
|
||||
Fetcher.fetch_object_from_id("https://example.com/objects/invalid-content-type")
|
||||
end
|
||||
|
||||
test "it accepts objects with application/ld+json and ActivityStreams profile" do
|
||||
# Setup a mock for an object with ld+json content-type and AS profile
|
||||
mock(fn
|
||||
%{method: :get, url: "https://example.com/objects/valid-ld-json"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
headers: [
|
||||
{"content-type",
|
||||
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""}
|
||||
],
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
"id" => "https://example.com/objects/valid-ld-json",
|
||||
"type" => "Note",
|
||||
"content" => "This has a valid ld+json content type",
|
||||
"actor" => "https://example.com/users/actor",
|
||||
"attributedTo" => "https://example.com/users/actor"
|
||||
})
|
||||
}
|
||||
end)
|
||||
|
||||
# This should pass if content-type validation works correctly
|
||||
assert {:ok, object} =
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(
|
||||
"https://example.com/objects/valid-ld-json"
|
||||
)
|
||||
|
||||
assert object["content"] == "This has a valid ld+json content type"
|
||||
end
|
||||
|
||||
test "it rejects objects with no content-type header" do
|
||||
# Setup a mock for an object with no content-type header
|
||||
mock(fn
|
||||
%{method: :get, url: "https://example.com/objects/no-content-type"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
# No content-type header
|
||||
headers: [],
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
"id" => "https://example.com/objects/no-content-type",
|
||||
"type" => "Note",
|
||||
"content" => "This has no content type header",
|
||||
"actor" => "https://example.com/users/actor",
|
||||
"attributedTo" => "https://example.com/users/actor"
|
||||
})
|
||||
}
|
||||
end)
|
||||
|
||||
# We want to test that the request fails with a missing content-type error
|
||||
# but the actual error is {:fetch, {:error, nil}} - we'll check for this format
|
||||
result = Fetcher.fetch_object_from_id("https://example.com/objects/no-content-type")
|
||||
assert {:fetch, {:error, nil}} = result
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -532,6 +607,110 @@ defmodule Pleroma.Object.FetcherTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "cross-domain redirect handling" do
|
||||
setup do
|
||||
mock(fn
|
||||
# Cross-domain redirect with original domain in id
|
||||
%{method: :get, url: "https://original.test/objects/123"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
url: "https://media.test/objects/123",
|
||||
headers: [{"content-type", "application/activity+json"}],
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
"id" => "https://original.test/objects/123",
|
||||
"type" => "Note",
|
||||
"content" => "This is redirected content",
|
||||
"actor" => "https://original.test/users/actor",
|
||||
"attributedTo" => "https://original.test/users/actor"
|
||||
})
|
||||
}
|
||||
|
||||
# Cross-domain redirect with final domain in id
|
||||
%{method: :get, url: "https://original.test/objects/final-domain-id"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
url: "https://media.test/objects/final-domain-id",
|
||||
headers: [{"content-type", "application/activity+json"}],
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
"id" => "https://media.test/objects/final-domain-id",
|
||||
"type" => "Note",
|
||||
"content" => "This has final domain in id",
|
||||
"actor" => "https://original.test/users/actor",
|
||||
"attributedTo" => "https://original.test/users/actor"
|
||||
})
|
||||
}
|
||||
|
||||
# No redirect - same domain
|
||||
%{method: :get, url: "https://original.test/objects/same-domain-redirect"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
url: "https://original.test/objects/different-path",
|
||||
headers: [{"content-type", "application/activity+json"}],
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
"id" => "https://original.test/objects/same-domain-redirect",
|
||||
"type" => "Note",
|
||||
"content" => "This has a same-domain redirect",
|
||||
"actor" => "https://original.test/users/actor",
|
||||
"attributedTo" => "https://original.test/users/actor"
|
||||
})
|
||||
}
|
||||
|
||||
# Test case with missing url field in response (common in tests)
|
||||
%{method: :get, url: "https://original.test/objects/missing-url"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
# No url field
|
||||
headers: [{"content-type", "application/activity+json"}],
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
"id" => "https://original.test/objects/missing-url",
|
||||
"type" => "Note",
|
||||
"content" => "This has no URL field in response",
|
||||
"actor" => "https://original.test/users/actor",
|
||||
"attributedTo" => "https://original.test/users/actor"
|
||||
})
|
||||
}
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "it rejects objects from cross-domain redirects with original domain in id" do
|
||||
assert {:error, {:cross_domain_redirect, true}} =
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(
|
||||
"https://original.test/objects/123"
|
||||
)
|
||||
end
|
||||
|
||||
test "it rejects objects from cross-domain redirects with final domain in id" do
|
||||
assert {:error, {:cross_domain_redirect, true}} =
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(
|
||||
"https://original.test/objects/final-domain-id"
|
||||
)
|
||||
end
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
assert data["content"] == "This has a 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(
|
||||
"https://original.test/objects/missing-url"
|
||||
)
|
||||
|
||||
assert data["content"] == "This has no URL field in response"
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch with history" do
|
||||
setup do
|
||||
object2 = %{
|
||||
|
|
|
|||
|
|
@ -6,12 +6,10 @@ defmodule Pleroma.ObjectTest do
|
|||
use Pleroma.DataCase
|
||||
use Oban.Testing, repo: Pleroma.Repo
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
import Mox
|
||||
import Pleroma.Factory
|
||||
import Tesla.Mock
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Hashtag
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
|
|
@ -158,7 +156,7 @@ defmodule Pleroma.ObjectTest do
|
|||
|
||||
uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
|
||||
|
||||
File.mkdir_p!(uploads_dir)
|
||||
Pleroma.Backports.mkdir_p!(uploads_dir)
|
||||
|
||||
file = %Plug.Upload{
|
||||
content_type: "image/jpeg",
|
||||
|
|
@ -176,8 +174,9 @@ defmodule Pleroma.ObjectTest do
|
|||
|
||||
filename = Path.basename(href)
|
||||
|
||||
assert {:ok, files} = File.ls(uploads_dir)
|
||||
assert filename in files
|
||||
expected_path = Path.join([uploads_dir, Pleroma.Upload.Filter.Dedupe.shard_path(filename)])
|
||||
|
||||
assert File.exists?(expected_path)
|
||||
|
||||
Object.delete(note)
|
||||
|
||||
|
|
@ -185,8 +184,7 @@ defmodule Pleroma.ObjectTest do
|
|||
|
||||
assert Object.get_by_id(note.id).data["deleted"]
|
||||
assert Object.get_by_id(attachment.id) == nil
|
||||
assert {:ok, files} = File.ls(uploads_dir)
|
||||
refute filename in files
|
||||
refute File.exists?(expected_path)
|
||||
end
|
||||
|
||||
test "with objects that have legacy data.url attribute" do
|
||||
|
|
@ -282,148 +280,6 @@ defmodule Pleroma.ObjectTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "get_by_id_and_maybe_refetch" do
|
||||
setup do
|
||||
mock(fn
|
||||
%{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: File.read!("test/fixtures/tesla_mock/poll_original.json"),
|
||||
headers: HttpRequestMock.activitypub_object_headers()
|
||||
}
|
||||
|
||||
env ->
|
||||
apply(HttpRequestMock, :request, [env])
|
||||
end)
|
||||
|
||||
mock_modified = fn resp ->
|
||||
mock(fn
|
||||
%{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
|
||||
resp
|
||||
|
||||
env ->
|
||||
apply(HttpRequestMock, :request, [env])
|
||||
end)
|
||||
end
|
||||
|
||||
on_exit(fn -> mock(fn env -> apply(HttpRequestMock, :request, [env]) end) end)
|
||||
|
||||
[mock_modified: mock_modified]
|
||||
end
|
||||
|
||||
test "refetches if the time since the last refetch is greater than the interval", %{
|
||||
mock_modified: mock_modified
|
||||
} do
|
||||
%Object{} =
|
||||
object =
|
||||
Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
|
||||
fetch: true
|
||||
)
|
||||
|
||||
Object.set_cache(object)
|
||||
|
||||
assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
|
||||
assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
|
||||
|
||||
mock_modified.(%Tesla.Env{
|
||||
status: 200,
|
||||
body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
|
||||
headers: HttpRequestMock.activitypub_object_headers()
|
||||
})
|
||||
|
||||
updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
|
||||
object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
|
||||
assert updated_object == object_in_cache
|
||||
assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
|
||||
assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
|
||||
end
|
||||
|
||||
test "returns the old object if refetch fails", %{mock_modified: mock_modified} do
|
||||
%Object{} =
|
||||
object =
|
||||
Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
|
||||
fetch: true
|
||||
)
|
||||
|
||||
Object.set_cache(object)
|
||||
|
||||
assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
|
||||
assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
|
||||
|
||||
assert capture_log(fn ->
|
||||
mock_modified.(%Tesla.Env{status: 404, body: ""})
|
||||
|
||||
updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
|
||||
object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
|
||||
assert updated_object == object_in_cache
|
||||
assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
|
||||
assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
|
||||
end) =~
|
||||
"[error] Couldn't refresh https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"
|
||||
end
|
||||
|
||||
test "does not refetch if the time since the last refetch is greater than the interval", %{
|
||||
mock_modified: mock_modified
|
||||
} do
|
||||
%Object{} =
|
||||
object =
|
||||
Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
|
||||
fetch: true
|
||||
)
|
||||
|
||||
Object.set_cache(object)
|
||||
|
||||
assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
|
||||
assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
|
||||
|
||||
mock_modified.(%Tesla.Env{
|
||||
status: 200,
|
||||
body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
|
||||
headers: HttpRequestMock.activitypub_object_headers()
|
||||
})
|
||||
|
||||
updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: 100)
|
||||
object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
|
||||
assert updated_object == object_in_cache
|
||||
assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
|
||||
assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
|
||||
end
|
||||
|
||||
test "preserves internal fields on refetch", %{mock_modified: mock_modified} do
|
||||
%Object{} =
|
||||
object =
|
||||
Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
|
||||
fetch: true
|
||||
)
|
||||
|
||||
Object.set_cache(object)
|
||||
|
||||
assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
|
||||
assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
|
||||
|
||||
user = insert(:user)
|
||||
activity = Activity.get_create_by_object_ap_id(object.data["id"])
|
||||
{:ok, activity} = CommonAPI.favorite(user, activity.id)
|
||||
object = Object.get_by_ap_id(activity.data["object"])
|
||||
|
||||
assert object.data["like_count"] == 1
|
||||
|
||||
mock_modified.(%Tesla.Env{
|
||||
status: 200,
|
||||
body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
|
||||
headers: HttpRequestMock.activitypub_object_headers()
|
||||
})
|
||||
|
||||
updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
|
||||
object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
|
||||
assert updated_object == object_in_cache
|
||||
assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
|
||||
assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
|
||||
|
||||
assert updated_object.data["like_count"] == 1
|
||||
end
|
||||
end
|
||||
|
||||
describe ":hashtags association" do
|
||||
test "Hashtag records are created with Object record and updated on its change" do
|
||||
user = insert(:user)
|
||||
|
|
|
|||
|
|
@ -1,42 +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.OTPVersionTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Pleroma.OTPVersion
|
||||
|
||||
describe "check/1" do
|
||||
test "22.4" do
|
||||
assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.4"]) ==
|
||||
"22.4"
|
||||
end
|
||||
|
||||
test "22.1" do
|
||||
assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.1"]) ==
|
||||
"22.1"
|
||||
end
|
||||
|
||||
test "21.1" do
|
||||
assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/21.1"]) ==
|
||||
"21.1"
|
||||
end
|
||||
|
||||
test "23.0" do
|
||||
assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/23.0"]) ==
|
||||
"23.0"
|
||||
end
|
||||
|
||||
test "with nonexistent file" do
|
||||
assert OTPVersion.get_version_from_files([
|
||||
"test/fixtures/warnings/otp_version/non-exising",
|
||||
"test/fixtures/warnings/otp_version/22.4"
|
||||
]) == "22.4"
|
||||
end
|
||||
|
||||
test "empty paths" do
|
||||
assert OTPVersion.get_version_from_files([]) == nil
|
||||
end
|
||||
end
|
||||
end
|
||||
293
test/pleroma/pleroma_ctl_test.exs
Normal file
293
test/pleroma/pleroma_ctl_test.exs
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.PleromaCtlTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
@pleroma_ctl Path.expand("../../rel/files/bin/pleroma_ctl", __DIR__)
|
||||
|
||||
setup do
|
||||
tmp_dir =
|
||||
Path.join(System.tmp_dir!(), "pleroma_ctl_test_#{System.unique_integer([:positive])}")
|
||||
|
||||
release_root = Path.join(tmp_dir, "release")
|
||||
bin_dir = Path.join(release_root, "bin")
|
||||
|
||||
File.mkdir_p!(bin_dir)
|
||||
File.cp!(@pleroma_ctl, Path.join(bin_dir, "pleroma_ctl"))
|
||||
File.chmod!(Path.join(bin_dir, "pleroma_ctl"), 0o755)
|
||||
|
||||
on_exit(fn -> File.rm_rf!(tmp_dir) end)
|
||||
|
||||
{:ok, tmp_dir: tmp_dir, release_root: release_root, bin_dir: bin_dir}
|
||||
end
|
||||
|
||||
test "update downloads branch-scoped latest OTP package", %{
|
||||
tmp_dir: tmp_dir,
|
||||
bin_dir: bin_dir,
|
||||
release_root: release_root
|
||||
} do
|
||||
stubs_dir = Path.join(tmp_dir, "stubs")
|
||||
curl_args_path = Path.join(tmp_dir, "curl.args")
|
||||
File.mkdir_p!(stubs_dir)
|
||||
write_update_stubs(stubs_dir, curl_args_path, "unused", "glibc")
|
||||
|
||||
update_tmp_dir = Path.join(tmp_dir, "update_tmp")
|
||||
File.mkdir_p!(update_tmp_dir)
|
||||
|
||||
{output, status} =
|
||||
System.cmd(
|
||||
Path.join(bin_dir, "pleroma_ctl"),
|
||||
[
|
||||
"update",
|
||||
"--branch",
|
||||
"develop",
|
||||
"--flavour",
|
||||
"amd64",
|
||||
"--tmp-dir",
|
||||
update_tmp_dir,
|
||||
"--no-rm"
|
||||
],
|
||||
env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}],
|
||||
stderr_to_stdout: true
|
||||
)
|
||||
|
||||
assert status == 0, output
|
||||
assert File.exists?(Path.join(release_root, "bin/marker"))
|
||||
|
||||
assert ["-fL", url, "-o", artifact_path] =
|
||||
curl_args_path
|
||||
|> File.read!()
|
||||
|> String.split("\n", trim: true)
|
||||
|
||||
assert url ==
|
||||
"https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-develop-amd64/latest/pleroma.zip"
|
||||
|
||||
assert artifact_path == Path.join(update_tmp_dir, "pleroma.zip")
|
||||
end
|
||||
|
||||
test "update detects stable branch and local flavour", %{
|
||||
tmp_dir: tmp_dir,
|
||||
bin_dir: bin_dir,
|
||||
release_root: release_root
|
||||
} do
|
||||
stubs_dir = Path.join(tmp_dir, "stubs")
|
||||
curl_args_path = Path.join(tmp_dir, "curl.args")
|
||||
File.mkdir_p!(stubs_dir)
|
||||
write_update_stubs(stubs_dir, curl_args_path, "x86_64", "glibc")
|
||||
write_start_erl_data(release_root, "2.10.0")
|
||||
|
||||
{output, status} =
|
||||
System.cmd(
|
||||
Path.join(bin_dir, "pleroma_ctl"),
|
||||
["update", "--tmp-dir", create_update_tmp_dir(tmp_dir), "--no-rm"],
|
||||
env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}],
|
||||
stderr_to_stdout: true
|
||||
)
|
||||
|
||||
assert status == 0, output
|
||||
|
||||
assert curl_url(curl_args_path) ==
|
||||
"https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-stable-amd64/latest/pleroma.zip"
|
||||
end
|
||||
|
||||
test "update detects develop branch and musl arm flavour", %{
|
||||
tmp_dir: tmp_dir,
|
||||
bin_dir: bin_dir,
|
||||
release_root: release_root
|
||||
} do
|
||||
stubs_dir = Path.join(tmp_dir, "stubs")
|
||||
curl_args_path = Path.join(tmp_dir, "curl.args")
|
||||
File.mkdir_p!(stubs_dir)
|
||||
write_update_stubs(stubs_dir, curl_args_path, "armv7l", "musl")
|
||||
write_start_erl_data(release_root, "2.10.0.develop")
|
||||
|
||||
{output, status} =
|
||||
System.cmd(
|
||||
Path.join(bin_dir, "pleroma_ctl"),
|
||||
["update", "--tmp-dir", create_update_tmp_dir(tmp_dir), "--no-rm"],
|
||||
env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}],
|
||||
stderr_to_stdout: true
|
||||
)
|
||||
|
||||
assert status == 0, output
|
||||
|
||||
assert curl_url(curl_args_path) ==
|
||||
"https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-develop-arm-musl/latest/pleroma.zip"
|
||||
end
|
||||
|
||||
test "update with zip URL bypasses branch and flavour detection", %{
|
||||
tmp_dir: tmp_dir,
|
||||
bin_dir: bin_dir,
|
||||
release_root: release_root
|
||||
} do
|
||||
stubs_dir = Path.join(tmp_dir, "stubs")
|
||||
curl_args_path = Path.join(tmp_dir, "curl.args")
|
||||
File.mkdir_p!(stubs_dir)
|
||||
write_update_stubs(stubs_dir, curl_args_path, "unsupported-arch", "unsupported-libc")
|
||||
write_start_erl_data(release_root, "2.10.0.custombranch")
|
||||
|
||||
custom_url = "https://example.test/custom.zip"
|
||||
|
||||
{output, status} =
|
||||
System.cmd(
|
||||
Path.join(bin_dir, "pleroma_ctl"),
|
||||
[
|
||||
"update",
|
||||
"--zip-url",
|
||||
custom_url,
|
||||
"--tmp-dir",
|
||||
create_update_tmp_dir(tmp_dir),
|
||||
"--no-rm"
|
||||
],
|
||||
env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}],
|
||||
stderr_to_stdout: true
|
||||
)
|
||||
|
||||
assert status == 0, output
|
||||
assert curl_url(curl_args_path) == custom_url
|
||||
end
|
||||
|
||||
test "passes arguments with spaces and Elixir string metacharacters", %{
|
||||
tmp_dir: tmp_dir,
|
||||
bin_dir: bin_dir
|
||||
} do
|
||||
capture_path = Path.join(tmp_dir, "captured_args")
|
||||
eval_path = Path.join(tmp_dir, "pleroma_ctl_eval.exs")
|
||||
|
||||
write_executable(Path.join(bin_dir, "pleroma"), """
|
||||
#!/bin/sh
|
||||
{
|
||||
printf '%s\n' 'defmodule Pleroma.ReleaseTasks do'
|
||||
printf '%s\n' ' def run(args), do: File.write!(System.fetch_env!("PLEROMA_CTL_CAPTURE"), :erlang.term_to_binary(args))'
|
||||
printf '%s\n' 'end'
|
||||
printf '%s\n' "$2"
|
||||
} > "$PLEROMA_CTL_EVAL_FILE"
|
||||
|
||||
exec elixir "$PLEROMA_CTL_EVAL_FILE"
|
||||
""")
|
||||
|
||||
{output, status} =
|
||||
System.cmd(
|
||||
Path.join(bin_dir, "pleroma_ctl"),
|
||||
[
|
||||
"user",
|
||||
"",
|
||||
"has space",
|
||||
~s(has "quote"),
|
||||
~s(has \\ backslash),
|
||||
~S(#{:not_interpolated})
|
||||
],
|
||||
env: [
|
||||
{"PLEROMA_CTL_CAPTURE", capture_path},
|
||||
{"PLEROMA_CTL_EVAL_FILE", eval_path}
|
||||
],
|
||||
stderr_to_stdout: true
|
||||
)
|
||||
|
||||
assert status == 0, output
|
||||
|
||||
assert capture_path
|
||||
|> File.read!()
|
||||
|> :erlang.binary_to_term() == [
|
||||
"user",
|
||||
"",
|
||||
"has space",
|
||||
~s(has "quote"),
|
||||
~s(has \\ backslash),
|
||||
~S(#{:not_interpolated})
|
||||
]
|
||||
end
|
||||
|
||||
defp write_executable(path, contents) do
|
||||
File.write!(path, contents)
|
||||
File.chmod!(path, 0o755)
|
||||
end
|
||||
|
||||
defp write_start_erl_data(release_root, version) do
|
||||
releases_dir = Path.join(release_root, "releases")
|
||||
File.mkdir_p!(releases_dir)
|
||||
File.write!(Path.join(releases_dir, "start_erl.data"), "erts-15.0 #{version}\n")
|
||||
end
|
||||
|
||||
defp create_update_tmp_dir(tmp_dir) do
|
||||
update_tmp_dir = Path.join(tmp_dir, "update_tmp")
|
||||
File.mkdir_p!(update_tmp_dir)
|
||||
update_tmp_dir
|
||||
end
|
||||
|
||||
defp write_update_stubs(stubs_dir, curl_args_path, arch, libc) do
|
||||
write_executable(Path.join(stubs_dir, "curl"), """
|
||||
#!/bin/sh
|
||||
printf '%s\n' "$@" > "#{curl_args_path}"
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-o)
|
||||
artifact="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
: > "$artifact"
|
||||
""")
|
||||
|
||||
write_executable(Path.join(stubs_dir, "unzip"), """
|
||||
#!/bin/sh
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-d)
|
||||
dest="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
mkdir -p "$dest/release/bin"
|
||||
printf 'marker' > "$dest/release/bin/marker"
|
||||
""")
|
||||
|
||||
write_executable(Path.join(stubs_dir, "uname"), """
|
||||
#!/bin/sh
|
||||
printf '%s\n' '#{arch}'
|
||||
""")
|
||||
|
||||
write_executable(Path.join(stubs_dir, "getconf"), getconf_stub(libc))
|
||||
|
||||
write_executable(Path.join(stubs_dir, "ldd"), """
|
||||
#!/bin/sh
|
||||
printf '%s\n' 'musl libc (mock)'
|
||||
""")
|
||||
end
|
||||
|
||||
defp getconf_stub("glibc") do
|
||||
"""
|
||||
#!/bin/sh
|
||||
printf '%s\n' 'glibc 2.40'
|
||||
"""
|
||||
end
|
||||
|
||||
defp getconf_stub(_libc) do
|
||||
"""
|
||||
#!/bin/sh
|
||||
exit 1
|
||||
"""
|
||||
end
|
||||
|
||||
defp curl_url(curl_args_path) do
|
||||
["-fL", url, "-o", _artifact_path] =
|
||||
curl_args_path
|
||||
|> File.read!()
|
||||
|> String.split("\n", trim: true)
|
||||
|
||||
url
|
||||
end
|
||||
end
|
||||
19
test/pleroma/release_task_test.exs
Normal file
19
test/pleroma/release_task_test.exs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.ReleaseTaskTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.ReleaseTasks
|
||||
|
||||
test "finding the module" do
|
||||
task = "search.meilisearch"
|
||||
assert Mix.Tasks.Pleroma.Search.Meilisearch == ReleaseTasks.find_module(task)
|
||||
|
||||
task = "user"
|
||||
assert Mix.Tasks.Pleroma.User == ReleaseTasks.find_module(task)
|
||||
|
||||
refute ReleaseTasks.find_module("doesnt.exist")
|
||||
end
|
||||
end
|
||||
|
|
@ -3,12 +3,11 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Repo.Migrations.AutolinkerToLinkifyTest do
|
||||
use Pleroma.DataCase
|
||||
use Pleroma.DataCase, async: false
|
||||
import Pleroma.Factory
|
||||
import Pleroma.Tests.Helpers
|
||||
alias Pleroma.ConfigDB
|
||||
|
||||
setup do: clear_config(Pleroma.Formatter)
|
||||
setup_all do: require_migration("20200716195806_autolinker_to_linkify")
|
||||
|
||||
test "change/0 converts auto_linker opts for Pleroma.Formatter", %{migration: migration} do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Repo.Migrations.PublisherMigrationChangeTest do
|
||||
use Oban.Testing, repo: Pleroma.Repo
|
||||
use Pleroma.DataCase
|
||||
import Pleroma.Factory
|
||||
import Pleroma.Tests.Helpers
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Workers.PublisherWorker
|
||||
|
||||
setup_all do: require_migration("20240729163838_publisher_job_change")
|
||||
|
||||
describe "up/0" do
|
||||
test "migrates publisher jobs to new format", %{migration: migration} do
|
||||
user = insert(:user)
|
||||
|
||||
%Activity{id: activity_id, data: %{"id" => ap_id}} =
|
||||
insert(:note_activity, user: user)
|
||||
|
||||
{:ok, %{id: job_id}} =
|
||||
PublisherWorker.new(%{
|
||||
"actor_id" => user.id,
|
||||
"json" => "{}",
|
||||
"id" => ap_id,
|
||||
"inbox" => "https://example.com/inbox",
|
||||
"unreachable_since" => nil
|
||||
})
|
||||
|> Oban.insert()
|
||||
|
||||
assert [%{id: ^job_id, args: %{"id" => ^ap_id}}] = all_enqueued(worker: PublisherWorker)
|
||||
|
||||
assert migration.up() == :ok
|
||||
|
||||
assert_enqueued(
|
||||
worker: PublisherWorker,
|
||||
args: %{"id" => ap_id, "activity_id" => activity_id}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -24,7 +24,8 @@ defmodule Pleroma.RepoTest do
|
|||
describe "get_assoc/2" do
|
||||
test "get assoc from preloaded data" do
|
||||
user = %User{name: "Agent Smith"}
|
||||
token = %Pleroma.Web.OAuth.Token{insert(:oauth_token) | user: user}
|
||||
%Pleroma.Web.OAuth.Token{} = token = insert(:oauth_token)
|
||||
token = %{token | user: user}
|
||||
assert Repo.get_assoc(token, :user) == {:ok, user}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ defmodule Pleroma.ResilienceTest do
|
|||
other_user = insert(:user)
|
||||
|
||||
{:ok, post_one} = CommonAPI.post(user, %{status: "Here is a post"})
|
||||
{:ok, like} = CommonAPI.favorite(other_user, post_one.id)
|
||||
{:ok, like} = CommonAPI.favorite(post_one.id, other_user)
|
||||
|
||||
%{
|
||||
user: user,
|
||||
|
|
@ -90,7 +90,7 @@ defmodule Pleroma.ResilienceTest do
|
|||
|> json_response(200)
|
||||
|
||||
# Favoriting again doesn't hurt
|
||||
{:ok, _like_two} = CommonAPI.favorite(other_user, post.id)
|
||||
{:ok, _like_two} = CommonAPI.favorite(post.id, other_user)
|
||||
|
||||
post = Repo.get(Activity, post.id)
|
||||
|
||||
|
|
|
|||
22
test/pleroma/reverse_proxy/client/tesla_test.exs
Normal file
22
test/pleroma/reverse_proxy/client/tesla_test.exs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.ReverseProxy.Client.TeslaTest do
|
||||
use ExUnit.Case
|
||||
|
||||
import Mox
|
||||
|
||||
alias Pleroma.ReverseProxy.Client.Tesla
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
test "cancels an unfinished stream before releasing its connection" do
|
||||
conn = self()
|
||||
stream = [make_ref(), make_ref()]
|
||||
|
||||
expect(Pleroma.GunMock, :cancel, fn ^conn, ^stream -> :ok end)
|
||||
|
||||
assert :ok = Tesla.close(%{pid: conn, stream: stream})
|
||||
end
|
||||
end
|
||||
|
|
@ -63,7 +63,11 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
|> Plug.Conn.put_req_header("user-agent", "fake/1.0")
|
||||
|> ReverseProxy.call("/user-agent")
|
||||
|
||||
assert json_response(conn, 200) == %{"user-agent" => Pleroma.Application.user_agent()}
|
||||
# Convert the response to a map without relying on json_response
|
||||
body = conn.resp_body
|
||||
assert conn.status == 200
|
||||
response = Jason.decode!(body)
|
||||
assert response == %{"user-agent" => Pleroma.Application.user_agent()}
|
||||
end
|
||||
|
||||
test "closed connection", %{conn: conn} do
|
||||
|
|
@ -111,6 +115,7 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
describe "max_body" do
|
||||
test "length returns error if content-length more than option", %{conn: conn} do
|
||||
request_mock(0)
|
||||
expect(ClientMock, :close, fn _ -> :ok end)
|
||||
|
||||
assert capture_log(fn ->
|
||||
ReverseProxy.call(conn, "/huge-file", max_body_length: 4)
|
||||
|
|
@ -124,25 +129,45 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
end) == ""
|
||||
end
|
||||
|
||||
test "closes streamed responses with invalid status", %{conn: conn} do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/invalid-status", _, _, _ ->
|
||||
{:ok, 404, [], %{url: "/invalid-status"}}
|
||||
end)
|
||||
|> expect(:close, fn %{url: "/invalid-status"} -> :ok end)
|
||||
|
||||
ReverseProxy.call(conn, "/invalid-status")
|
||||
end
|
||||
|
||||
test "max_body_length returns error if streaming body more than that option", %{conn: conn} do
|
||||
stream_mock(3, true)
|
||||
|
||||
assert capture_log(fn ->
|
||||
ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 30)
|
||||
ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 29)
|
||||
end) =~
|
||||
"Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large"
|
||||
end
|
||||
|
||||
test "max_body_length accepts a body exactly at the limit", %{conn: conn} do
|
||||
stream_mock(4)
|
||||
|
||||
conn = ReverseProxy.call(conn, "/stream-bytes/30", max_body_length: 30)
|
||||
assert byte_size(conn.resp_body) == 30
|
||||
end
|
||||
end
|
||||
|
||||
describe "HEAD requests" do
|
||||
test "common", %{conn: conn} do
|
||||
ClientMock
|
||||
|> expect(:request, fn :head, "/head", _, _, _ ->
|
||||
{:ok, 200, [{"content-type", "text/html; charset=utf-8"}]}
|
||||
{:ok, 200, [{"content-type", "image/png"}]}
|
||||
end)
|
||||
|
||||
conn = ReverseProxy.call(Map.put(conn, :method, "HEAD"), "/head")
|
||||
assert html_response(conn, 200) == ""
|
||||
|
||||
assert conn.status == 200
|
||||
assert Conn.get_resp_header(conn, "content-type") == ["image/png"]
|
||||
assert conn.resp_body == ""
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -193,7 +218,10 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
|
||||
test "204", %{conn: conn} do
|
||||
url = "/status/204"
|
||||
expect(ClientMock, :request, fn :get, _url, _, _, _ -> {:ok, 204, [], %{}} end)
|
||||
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, _url, _, _, _ -> {:ok, 204, [], %{}} end)
|
||||
|> expect(:close, fn %{} -> :ok end)
|
||||
|
||||
capture_log(fn ->
|
||||
conn = ReverseProxy.call(conn, url)
|
||||
|
|
@ -215,6 +243,56 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"]
|
||||
end
|
||||
|
||||
test "sniffs and preserves an image body with a generic content type", %{conn: conn} do
|
||||
body = File.read!("test/fixtures/image.jpg")
|
||||
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/extensionless", headers, _, _ ->
|
||||
assert {"accept-encoding", "identity"} in headers
|
||||
{:ok, 200, [{"content-type", "application/octet-stream"}], %{body: body}}
|
||||
end)
|
||||
|> expect(:stream_body, fn %{body: ^body} = client ->
|
||||
{:ok, body, Map.delete(client, :body)}
|
||||
end)
|
||||
|> expect(:stream_body, fn %{} -> :done end)
|
||||
|
||||
conn =
|
||||
ReverseProxy.call(conn, "/extensionless",
|
||||
sniff_content_type: true,
|
||||
max_read_duration: :infinity,
|
||||
req_headers: [{"accept-encoding", "gzip"}]
|
||||
)
|
||||
|
||||
assert conn.resp_body == body
|
||||
assert Conn.get_resp_header(conn, "content-type") == ["image/jpeg"]
|
||||
|
||||
assert Conn.get_resp_header(conn, "content-disposition") == [
|
||||
"inline; filename=\"inline.jpg\""
|
||||
]
|
||||
end
|
||||
|
||||
test "preserves a generic content type for a non-image body", %{conn: conn} do
|
||||
body = "not an image"
|
||||
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/extensionless", _, _, _ ->
|
||||
{:ok, 200, [], %{body: body}}
|
||||
end)
|
||||
|> expect(:stream_body, fn %{body: ^body} = client ->
|
||||
{:ok, body, Map.delete(client, :body)}
|
||||
end)
|
||||
|> expect(:stream_body, fn %{} -> :done end)
|
||||
|
||||
conn = ReverseProxy.call(conn, "/extensionless", sniff_content_type: true)
|
||||
|
||||
assert conn.resp_body == body
|
||||
assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"]
|
||||
|
||||
assert Conn.get_resp_header(conn, "content-disposition") == [
|
||||
"attachment; filename=\"extensionless\""
|
||||
]
|
||||
end
|
||||
|
||||
defp headers_mock(_) do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/headers", headers, _, _ ->
|
||||
|
|
@ -249,7 +327,10 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
)
|
||||
|> ReverseProxy.call("/headers")
|
||||
|
||||
%{"headers" => headers} = json_response(conn, 200)
|
||||
body = conn.resp_body
|
||||
assert conn.status == 200
|
||||
response = Jason.decode!(body)
|
||||
headers = response["headers"]
|
||||
assert headers["Accept"] == "text/html"
|
||||
end
|
||||
|
||||
|
|
@ -262,7 +343,10 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
)
|
||||
|> ReverseProxy.call("/headers")
|
||||
|
||||
%{"headers" => headers} = json_response(conn, 200)
|
||||
body = conn.resp_body
|
||||
assert conn.status == 200
|
||||
response = Jason.decode!(body)
|
||||
headers = response["headers"]
|
||||
refute headers["Accept-Language"]
|
||||
end
|
||||
end
|
||||
|
|
@ -281,7 +365,7 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
|> expect(:stream_body, fn _ -> :done end)
|
||||
|
||||
conn = ReverseProxy.call(conn, "/cache")
|
||||
assert {"cache-control", "public, max-age=1209600"} in conn.resp_headers
|
||||
assert {"cache-control", "public, max-age=1209600, immutable"} in conn.resp_headers
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -315,6 +399,24 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
conn = ReverseProxy.call(conn, "/disposition")
|
||||
|
||||
assert {"content-type", "image/gif"} in conn.resp_headers
|
||||
assert {"content-disposition", "inline; filename=\"inline.gif\""} in conn.resp_headers
|
||||
end
|
||||
|
||||
test "forces inline for inline content types overriding upstream attachment", %{
|
||||
conn: conn
|
||||
} do
|
||||
disposition_headers_mock([
|
||||
{"content-type", "image/png"},
|
||||
{"content-disposition", "attachment; filename=\"filename.png\""},
|
||||
{"content-disposition", "attachment; filename=\"duplicate.png\""},
|
||||
{"content-length", "0"}
|
||||
])
|
||||
|
||||
conn = ReverseProxy.call(conn, "/disposition")
|
||||
|
||||
[disposition] = Conn.get_resp_header(conn, "content-disposition")
|
||||
assert String.starts_with?(disposition, "inline")
|
||||
refute String.starts_with?(disposition, "attachment")
|
||||
end
|
||||
|
||||
test "with content-disposition header", %{conn: conn} do
|
||||
|
|
@ -327,5 +429,166 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
|
||||
assert {"content-disposition", "attachment; filename=\"filename.jpg\""} in conn.resp_headers
|
||||
end
|
||||
|
||||
test "with inline_content_types: true leaves upstream headers untouched", %{
|
||||
conn: conn
|
||||
} do
|
||||
# opt == true: the proxy must not synthesise or rewrite content-disposition.
|
||||
disposition_headers_mock([
|
||||
{"content-type", "image/png"},
|
||||
{"content-disposition", "attachment; filename=\"upstream.png\""},
|
||||
{"content-length", "0"}
|
||||
])
|
||||
|
||||
conn = ReverseProxy.call(conn, "/disposition", inline_content_types: true)
|
||||
|
||||
assert {"content-disposition", "attachment; filename=\"upstream.png\""} in conn.resp_headers
|
||||
end
|
||||
|
||||
test "forces bare inline for a whitelisted type with no MIME extension", %{
|
||||
conn: conn
|
||||
} do
|
||||
# image/x-foo-bar has no entry in MIME's database, so inline_filename/1
|
||||
# returns nil and the disposition should be the bare token "inline".
|
||||
disposition_headers_mock([
|
||||
{"content-type", "image/x-foo-bar"},
|
||||
{"content-length", "0"}
|
||||
])
|
||||
|
||||
conn = ReverseProxy.call(conn, "/disposition", inline_content_types: ["image/x-foo-bar"])
|
||||
|
||||
[disposition] = Conn.get_resp_header(conn, "content-disposition")
|
||||
assert disposition == "inline"
|
||||
end
|
||||
|
||||
test "serves modern browser image types inline", %{conn: conn} do
|
||||
disposition_headers_mock([
|
||||
{"content-type", "image/webp"},
|
||||
{"content-length", "0"}
|
||||
])
|
||||
|
||||
conn = ReverseProxy.call(conn, "/disposition")
|
||||
|
||||
assert Conn.get_resp_header(conn, "content-disposition") == [
|
||||
"inline; filename=\"inline.webp\""
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
describe "content-type sanitisation" do
|
||||
test "preserves allowed image type", %{conn: conn} do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/content", _, _, _ ->
|
||||
{:ok, 200, [{"content-type", "image/png"}], %{url: "/content"}}
|
||||
end)
|
||||
|> expect(:stream_body, fn _ -> :done end)
|
||||
|
||||
conn = ReverseProxy.call(conn, "/content")
|
||||
|
||||
assert conn.status == 200
|
||||
assert Conn.get_resp_header(conn, "content-type") == ["image/png"]
|
||||
end
|
||||
|
||||
test "preserves allowed video type", %{conn: conn} do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/content", _, _, _ ->
|
||||
{:ok, 200, [{"content-type", "video/mp4"}], %{url: "/content"}}
|
||||
end)
|
||||
|> expect(:stream_body, fn _ -> :done end)
|
||||
|
||||
conn = ReverseProxy.call(conn, "/content")
|
||||
|
||||
assert conn.status == 200
|
||||
assert Conn.get_resp_header(conn, "content-type") == ["video/mp4"]
|
||||
end
|
||||
|
||||
test "sanitizes ActivityPub content type", %{conn: conn} do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/content", _, _, _ ->
|
||||
{:ok, 200, [{"content-type", "application/activity+json"}], %{url: "/content"}}
|
||||
end)
|
||||
|> expect(:stream_body, fn _ -> :done end)
|
||||
|
||||
conn = ReverseProxy.call(conn, "/content")
|
||||
|
||||
assert conn.status == 200
|
||||
assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"]
|
||||
end
|
||||
|
||||
test "sanitizes LD-JSON content type", %{conn: conn} do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/content", _, _, _ ->
|
||||
{:ok, 200, [{"content-type", "application/ld+json"}], %{url: "/content"}}
|
||||
end)
|
||||
|> expect(:stream_body, fn _ -> :done end)
|
||||
|
||||
conn = ReverseProxy.call(conn, "/content")
|
||||
|
||||
assert conn.status == 200
|
||||
assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"]
|
||||
end
|
||||
end
|
||||
|
||||
# Hackney is used for Reverse Proxy when Hackney or Finch is the Tesla Adapter
|
||||
# Gun is able to proxy through Tesla, so it does not need testing as the
|
||||
# test cases in the Pleroma.HTTPTest module are sufficient
|
||||
describe "Hackney URL encoding:" do
|
||||
setup do
|
||||
ClientMock
|
||||
|> expect(:request, fn
|
||||
:get,
|
||||
"https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz",
|
||||
_headers,
|
||||
_body,
|
||||
_opts ->
|
||||
{:ok, 200, [{"content-type", "image/png"}], "It works!"}
|
||||
|
||||
:get,
|
||||
"https://example.com/media/foo/bar%20!$&'()*+,;=/:%20@a%20%5Bbaz%5D.mp4",
|
||||
_headers,
|
||||
_body,
|
||||
_opts ->
|
||||
{:ok, 200, [{"content-type", "video/mp4"}], "Allowed reserved chars."}
|
||||
|
||||
:get, "https://example.com/media/unicode%20%F0%9F%99%82%20.gif", _headers, _body, _opts ->
|
||||
{:ok, 200, [{"content-type", "image/gif"}], "Unicode emoji in path"}
|
||||
end)
|
||||
|> stub(:stream_body, fn _ -> :done end)
|
||||
|> stub(:close, fn _ -> :ok end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "properly encodes URLs with spaces", %{conn: conn} do
|
||||
url_with_space = "https://example.com/emoji/Pack 1/koronebless.png?foo=bar baz"
|
||||
|
||||
result = ReverseProxy.call(conn, url_with_space)
|
||||
|
||||
assert result.status == 200
|
||||
end
|
||||
|
||||
test "properly encoded URL should not be altered", %{conn: conn} do
|
||||
properly_encoded_url = "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz"
|
||||
|
||||
result = ReverseProxy.call(conn, properly_encoded_url)
|
||||
|
||||
assert result.status == 200
|
||||
end
|
||||
|
||||
test "properly encodes URLs with allowed reserved characters", %{conn: conn} do
|
||||
url_with_reserved_chars = "https://example.com/media/foo/bar !$&'()*+,;=/: @a [baz].mp4"
|
||||
|
||||
result = ReverseProxy.call(conn, url_with_reserved_chars)
|
||||
|
||||
assert result.status == 200
|
||||
end
|
||||
|
||||
test "properly encodes URLs with unicode in path", %{conn: conn} do
|
||||
url_with_unicode = "https://example.com/media/unicode 🙂 .gif"
|
||||
|
||||
result = ReverseProxy.call(conn, url_with_unicode)
|
||||
|
||||
assert result.status == 200
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
496
test/pleroma/safe_zip_test.exs
Normal file
496
test/pleroma/safe_zip_test.exs
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
defmodule Pleroma.SafeZipTest do
|
||||
# Not making this async because it creates and deletes files
|
||||
use ExUnit.Case
|
||||
|
||||
alias Pleroma.SafeZip
|
||||
|
||||
@fixtures_dir "test/fixtures"
|
||||
@tmp_dir "test/zip_tmp"
|
||||
|
||||
setup do
|
||||
# Ensure tmp directory exists
|
||||
Pleroma.Backports.mkdir_p!(@tmp_dir)
|
||||
|
||||
on_exit(fn ->
|
||||
# Clean up any files created during tests
|
||||
File.rm_rf!(@tmp_dir)
|
||||
Pleroma.Backports.mkdir_p!(@tmp_dir)
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
describe "list_dir_file/1" do
|
||||
test "lists files in a valid zip" do
|
||||
{:ok, files} = SafeZip.list_dir_file(Path.join(@fixtures_dir, "emojis.zip"))
|
||||
assert is_list(files)
|
||||
assert length(files) > 0
|
||||
end
|
||||
|
||||
test "returns an empty list for empty zip" do
|
||||
{:ok, files} = SafeZip.list_dir_file(Path.join(@fixtures_dir, "empty.zip"))
|
||||
assert files == []
|
||||
end
|
||||
|
||||
test "returns error for non-existent file" do
|
||||
assert {:error, _} = SafeZip.list_dir_file(Path.join(@fixtures_dir, "nonexistent.zip"))
|
||||
end
|
||||
|
||||
test "only lists regular files, not directories" do
|
||||
# Create a zip with both files and directories
|
||||
zip_path = create_zip_with_directory()
|
||||
|
||||
# List files with SafeZip
|
||||
{:ok, files} = SafeZip.list_dir_file(zip_path)
|
||||
|
||||
# Verify only regular files are listed, not directories
|
||||
assert "file_in_dir/test_file.txt" in files
|
||||
assert "root_file.txt" in files
|
||||
|
||||
# Directory entries should not be included in the list
|
||||
refute "file_in_dir/" in files
|
||||
end
|
||||
end
|
||||
|
||||
describe "contains_all_data?/2" do
|
||||
test "returns true when all files are in the archive" do
|
||||
# For this test, we'll create our own zip file with known content
|
||||
# to ensure we can test the contains_all_data? function properly
|
||||
zip_path = create_zip_with_directory()
|
||||
archive_data = File.read!(zip_path)
|
||||
|
||||
# Check if the archive contains the root file
|
||||
# Note: The function expects charlists (Erlang strings) in the MapSet
|
||||
assert SafeZip.contains_all_data?(archive_data, MapSet.new([~c"root_file.txt"]))
|
||||
end
|
||||
|
||||
test "returns false when files are missing" do
|
||||
archive_path = Path.join(@fixtures_dir, "emojis.zip")
|
||||
archive_data = File.read!(archive_path)
|
||||
|
||||
# Create a MapSet with non-existent files
|
||||
fset = MapSet.new([~c"nonexistent.txt"])
|
||||
|
||||
refute SafeZip.contains_all_data?(archive_data, fset)
|
||||
end
|
||||
|
||||
test "returns false for invalid archive data" do
|
||||
refute SafeZip.contains_all_data?("invalid data", MapSet.new([~c"file.txt"]))
|
||||
end
|
||||
|
||||
test "only checks for regular files, not directories" do
|
||||
# Create a zip with both files and directories
|
||||
zip_path = create_zip_with_directory()
|
||||
archive_data = File.read!(zip_path)
|
||||
|
||||
# Check if the archive contains a directory (should return false)
|
||||
refute SafeZip.contains_all_data?(archive_data, MapSet.new([~c"file_in_dir/"]))
|
||||
|
||||
# For this test, we'll manually check if the file exists in the archive
|
||||
# by extracting it and verifying it exists
|
||||
extract_dir = Path.join(@tmp_dir, "extract_check")
|
||||
Pleroma.Backports.mkdir_p!(extract_dir)
|
||||
{:ok, files} = SafeZip.unzip_file(zip_path, extract_dir)
|
||||
|
||||
# Verify the root file was extracted
|
||||
assert Enum.any?(files, fn file ->
|
||||
Path.basename(file) == "root_file.txt"
|
||||
end)
|
||||
|
||||
# Verify the file exists on disk
|
||||
assert File.exists?(Path.join(extract_dir, "root_file.txt"))
|
||||
end
|
||||
end
|
||||
|
||||
describe "zip/4" do
|
||||
test "creates a zip file on disk" do
|
||||
# Create a test file
|
||||
test_file_path = Path.join(@tmp_dir, "test_file.txt")
|
||||
File.write!(test_file_path, "test content")
|
||||
|
||||
# Create a zip file
|
||||
zip_path = Path.join(@tmp_dir, "test.zip")
|
||||
assert {:ok, ^zip_path} = SafeZip.zip(zip_path, ["test_file.txt"], @tmp_dir, false)
|
||||
|
||||
# Verify the zip file exists
|
||||
assert File.exists?(zip_path)
|
||||
end
|
||||
|
||||
test "creates a zip file in memory" do
|
||||
# Create a test file
|
||||
test_file_path = Path.join(@tmp_dir, "test_file.txt")
|
||||
File.write!(test_file_path, "test content")
|
||||
|
||||
# Create a zip file in memory
|
||||
zip_name = Path.join(@tmp_dir, "test.zip")
|
||||
|
||||
assert {:ok, {^zip_name, zip_data}} =
|
||||
SafeZip.zip(zip_name, ["test_file.txt"], @tmp_dir, true)
|
||||
|
||||
# Verify the zip data is binary
|
||||
assert is_binary(zip_data)
|
||||
end
|
||||
|
||||
test "returns error for unsafe paths" do
|
||||
# Try to zip a file with path traversal
|
||||
assert {:error, _} =
|
||||
SafeZip.zip(
|
||||
Path.join(@tmp_dir, "test.zip"),
|
||||
["../fixtures/test.txt"],
|
||||
@tmp_dir,
|
||||
false
|
||||
)
|
||||
end
|
||||
|
||||
test "can create zip with directories" do
|
||||
# Create a directory structure
|
||||
dir_path = Path.join(@tmp_dir, "test_dir")
|
||||
Pleroma.Backports.mkdir_p!(dir_path)
|
||||
|
||||
file_in_dir_path = Path.join(dir_path, "file_in_dir.txt")
|
||||
File.write!(file_in_dir_path, "file in directory")
|
||||
|
||||
# Create a zip file
|
||||
zip_path = Path.join(@tmp_dir, "dir_test.zip")
|
||||
|
||||
assert {:ok, ^zip_path} =
|
||||
SafeZip.zip(
|
||||
zip_path,
|
||||
["test_dir/file_in_dir.txt"],
|
||||
@tmp_dir,
|
||||
false
|
||||
)
|
||||
|
||||
# Verify the zip file exists
|
||||
assert File.exists?(zip_path)
|
||||
|
||||
# Extract and verify the directory structure is preserved
|
||||
extract_dir = Path.join(@tmp_dir, "extract")
|
||||
{:ok, files} = SafeZip.unzip_file(zip_path, extract_dir)
|
||||
|
||||
# Check if the file path is in the list, accounting for possible full paths
|
||||
assert Enum.any?(files, fn file ->
|
||||
String.ends_with?(file, "file_in_dir.txt")
|
||||
end)
|
||||
|
||||
# Verify the file exists in the expected location
|
||||
assert File.exists?(Path.join([extract_dir, "test_dir", "file_in_dir.txt"]))
|
||||
end
|
||||
end
|
||||
|
||||
describe "unzip_file/3" do
|
||||
test "extracts files from a zip archive" do
|
||||
archive_path = Path.join(@fixtures_dir, "emojis.zip")
|
||||
|
||||
# Extract the archive
|
||||
assert {:ok, files} = SafeZip.unzip_file(archive_path, @tmp_dir)
|
||||
|
||||
# Verify files were extracted
|
||||
assert is_list(files)
|
||||
assert length(files) > 0
|
||||
|
||||
# Verify at least one file exists
|
||||
first_file = List.first(files)
|
||||
|
||||
# Simply check that the file exists in the tmp directory
|
||||
assert File.exists?(first_file)
|
||||
end
|
||||
|
||||
test "extracts specific files from a zip archive" do
|
||||
archive_path = Path.join(@fixtures_dir, "emojis.zip")
|
||||
|
||||
# Get list of files in the archive
|
||||
{:ok, all_files} = SafeZip.list_dir_file(archive_path)
|
||||
file_to_extract = List.first(all_files)
|
||||
|
||||
# Extract only one file
|
||||
assert {:ok, [extracted_file]} =
|
||||
SafeZip.unzip_file(archive_path, @tmp_dir, [file_to_extract])
|
||||
|
||||
# Verify only the specified file was extracted
|
||||
assert Path.basename(extracted_file) == Path.basename(file_to_extract)
|
||||
|
||||
# Check that the file exists in the tmp directory
|
||||
assert File.exists?(Path.join(@tmp_dir, Path.basename(file_to_extract)))
|
||||
end
|
||||
|
||||
test "returns error for invalid zip file" do
|
||||
invalid_path = Path.join(@tmp_dir, "invalid.zip")
|
||||
File.write!(invalid_path, "not a zip file")
|
||||
|
||||
assert {:error, _} = SafeZip.unzip_file(invalid_path, @tmp_dir)
|
||||
end
|
||||
|
||||
test "creates directories when extracting files in subdirectories" do
|
||||
# Create a zip with files in subdirectories
|
||||
zip_path = create_zip_with_directory()
|
||||
|
||||
# Extract the archive
|
||||
assert {:ok, files} = SafeZip.unzip_file(zip_path, @tmp_dir)
|
||||
|
||||
# Verify files were extracted - handle both relative and absolute paths
|
||||
assert Enum.any?(files, fn file ->
|
||||
Path.basename(file) == "test_file.txt" &&
|
||||
String.contains?(file, "file_in_dir")
|
||||
end)
|
||||
|
||||
assert Enum.any?(files, fn file ->
|
||||
Path.basename(file) == "root_file.txt"
|
||||
end)
|
||||
|
||||
# Verify directory was created
|
||||
dir_path = Path.join(@tmp_dir, "file_in_dir")
|
||||
assert File.exists?(dir_path)
|
||||
assert File.dir?(dir_path)
|
||||
|
||||
# Verify file in directory was extracted
|
||||
file_path = Path.join(dir_path, "test_file.txt")
|
||||
assert File.exists?(file_path)
|
||||
end
|
||||
end
|
||||
|
||||
describe "unzip_data/3" do
|
||||
test "extracts files from zip data" do
|
||||
archive_path = Path.join(@fixtures_dir, "emojis.zip")
|
||||
archive_data = File.read!(archive_path)
|
||||
|
||||
# Extract the archive from data
|
||||
assert {:ok, files} = SafeZip.unzip_data(archive_data, @tmp_dir)
|
||||
|
||||
# Verify files were extracted
|
||||
assert is_list(files)
|
||||
assert length(files) > 0
|
||||
|
||||
# Verify at least one file exists
|
||||
first_file = List.first(files)
|
||||
|
||||
# Simply check that the file exists in the tmp directory
|
||||
assert File.exists?(first_file)
|
||||
end
|
||||
|
||||
test "extracts specific files from zip data" do
|
||||
archive_path = Path.join(@fixtures_dir, "emojis.zip")
|
||||
archive_data = File.read!(archive_path)
|
||||
|
||||
# Get list of files in the archive
|
||||
{:ok, all_files} = SafeZip.list_dir_file(archive_path)
|
||||
file_to_extract = List.first(all_files)
|
||||
|
||||
# Extract only one file
|
||||
assert {:ok, extracted_files} =
|
||||
SafeZip.unzip_data(archive_data, @tmp_dir, [file_to_extract])
|
||||
|
||||
# Verify only the specified file was extracted
|
||||
assert Enum.any?(extracted_files, fn path ->
|
||||
Path.basename(path) == Path.basename(file_to_extract)
|
||||
end)
|
||||
|
||||
# Simply check that the file exists in the tmp directory
|
||||
assert File.exists?(Path.join(@tmp_dir, Path.basename(file_to_extract)))
|
||||
end
|
||||
|
||||
test "returns error for invalid zip data" do
|
||||
assert {:error, _} = SafeZip.unzip_data("not a zip file", @tmp_dir)
|
||||
end
|
||||
|
||||
test "creates directories when extracting files in subdirectories from data" do
|
||||
# Create a zip with files in subdirectories
|
||||
zip_path = create_zip_with_directory()
|
||||
archive_data = File.read!(zip_path)
|
||||
|
||||
# Extract the archive from data
|
||||
assert {:ok, files} = SafeZip.unzip_data(archive_data, @tmp_dir)
|
||||
|
||||
# Verify files were extracted - handle both relative and absolute paths
|
||||
assert Enum.any?(files, fn file ->
|
||||
Path.basename(file) == "test_file.txt" &&
|
||||
String.contains?(file, "file_in_dir")
|
||||
end)
|
||||
|
||||
assert Enum.any?(files, fn file ->
|
||||
Path.basename(file) == "root_file.txt"
|
||||
end)
|
||||
|
||||
# Verify directory was created
|
||||
dir_path = Path.join(@tmp_dir, "file_in_dir")
|
||||
assert File.exists?(dir_path)
|
||||
assert File.dir?(dir_path)
|
||||
|
||||
# Verify file in directory was extracted
|
||||
file_path = Path.join(dir_path, "test_file.txt")
|
||||
assert File.exists?(file_path)
|
||||
end
|
||||
end
|
||||
|
||||
# Security tests
|
||||
describe "security checks" do
|
||||
test "prevents path traversal in zip extraction" do
|
||||
# Create a malicious zip file with path traversal
|
||||
malicious_zip_path = create_malicious_zip_with_path_traversal()
|
||||
|
||||
# Try to extract it with SafeZip
|
||||
assert {:error, _} = SafeZip.unzip_file(malicious_zip_path, @tmp_dir)
|
||||
|
||||
# Verify the file was not extracted outside the target directory
|
||||
refute File.exists?(Path.join(Path.dirname(@tmp_dir), "traversal_attempt.txt"))
|
||||
end
|
||||
|
||||
test "prevents directory traversal in zip listing" do
|
||||
# Create a malicious zip file with path traversal
|
||||
malicious_zip_path = create_malicious_zip_with_path_traversal()
|
||||
|
||||
# Try to list files with SafeZip
|
||||
assert {:error, _} = SafeZip.list_dir_file(malicious_zip_path)
|
||||
end
|
||||
|
||||
test "prevents path traversal in zip data extraction" do
|
||||
# Create a malicious zip file with path traversal
|
||||
malicious_zip_path = create_malicious_zip_with_path_traversal()
|
||||
malicious_data = File.read!(malicious_zip_path)
|
||||
|
||||
# Try to extract it with SafeZip
|
||||
assert {:error, _} = SafeZip.unzip_data(malicious_data, @tmp_dir)
|
||||
|
||||
# Verify the file was not extracted outside the target directory
|
||||
refute File.exists?(Path.join(Path.dirname(@tmp_dir), "traversal_attempt.txt"))
|
||||
end
|
||||
|
||||
test "handles zip bomb attempts" do
|
||||
# Create a zip bomb (a zip with many files or large files)
|
||||
zip_bomb_path = create_zip_bomb()
|
||||
|
||||
# The SafeZip module should handle this gracefully
|
||||
# Either by successfully extracting it (if it's not too large)
|
||||
# or by returning an error (if it detects a potential zip bomb)
|
||||
result = SafeZip.unzip_file(zip_bomb_path, @tmp_dir)
|
||||
|
||||
case result do
|
||||
{:ok, _} ->
|
||||
# If it successfully extracts, make sure it didn't fill up the disk
|
||||
# This is a simple check to ensure the extraction was controlled
|
||||
assert File.exists?(@tmp_dir)
|
||||
|
||||
{:error, _} ->
|
||||
# If it returns an error, that's also acceptable
|
||||
# The important thing is that it doesn't crash or hang
|
||||
assert true
|
||||
end
|
||||
end
|
||||
|
||||
test "handles deeply nested directory structures" do
|
||||
# Create a zip with deeply nested directories
|
||||
deep_nest_path = create_deeply_nested_zip()
|
||||
|
||||
# The SafeZip module should handle this gracefully
|
||||
result = SafeZip.unzip_file(deep_nest_path, @tmp_dir)
|
||||
|
||||
case result do
|
||||
{:ok, files} ->
|
||||
# If it successfully extracts, verify the files were extracted
|
||||
assert is_list(files)
|
||||
assert length(files) > 0
|
||||
|
||||
{:error, _} ->
|
||||
# If it returns an error, that's also acceptable
|
||||
# The important thing is that it doesn't crash or hang
|
||||
assert true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Helper functions to create test fixtures
|
||||
|
||||
# Creates a zip file with a path traversal attempt
|
||||
defp create_malicious_zip_with_path_traversal do
|
||||
malicious_zip_path = Path.join(@tmp_dir, "path_traversal.zip")
|
||||
|
||||
# Create a file to include in the zip
|
||||
test_file_path = Path.join(@tmp_dir, "test_file.txt")
|
||||
File.write!(test_file_path, "malicious content")
|
||||
|
||||
# Use Erlang's zip module directly to create a zip with path traversal
|
||||
{:ok, charlist_path} =
|
||||
:zip.create(
|
||||
String.to_charlist(malicious_zip_path),
|
||||
[{String.to_charlist("../traversal_attempt.txt"), File.read!(test_file_path)}]
|
||||
)
|
||||
|
||||
to_string(charlist_path)
|
||||
end
|
||||
|
||||
# Creates a zip file with directory entries
|
||||
defp create_zip_with_directory do
|
||||
zip_path = Path.join(@tmp_dir, "with_directory.zip")
|
||||
|
||||
# Create files to include in the zip
|
||||
root_file_path = Path.join(@tmp_dir, "root_file.txt")
|
||||
File.write!(root_file_path, "root file content")
|
||||
|
||||
# Create a directory and a file in it
|
||||
dir_path = Path.join(@tmp_dir, "file_in_dir")
|
||||
Pleroma.Backports.mkdir_p!(dir_path)
|
||||
|
||||
file_in_dir_path = Path.join(dir_path, "test_file.txt")
|
||||
File.write!(file_in_dir_path, "file in directory content")
|
||||
|
||||
# Use Erlang's zip module to create a zip with directory structure
|
||||
{:ok, charlist_path} =
|
||||
:zip.create(
|
||||
String.to_charlist(zip_path),
|
||||
[
|
||||
{String.to_charlist("root_file.txt"), File.read!(root_file_path)},
|
||||
{String.to_charlist("file_in_dir/test_file.txt"), File.read!(file_in_dir_path)}
|
||||
]
|
||||
)
|
||||
|
||||
to_string(charlist_path)
|
||||
end
|
||||
|
||||
# Creates a zip bomb (a zip with many small files)
|
||||
defp create_zip_bomb do
|
||||
zip_path = Path.join(@tmp_dir, "zip_bomb.zip")
|
||||
|
||||
# Create a small file to duplicate many times
|
||||
small_file_path = Path.join(@tmp_dir, "small_file.txt")
|
||||
File.write!(small_file_path, String.duplicate("A", 100))
|
||||
|
||||
# Create a list of many files to include in the zip
|
||||
file_entries =
|
||||
for i <- 1..100 do
|
||||
{String.to_charlist("file_#{i}.txt"), File.read!(small_file_path)}
|
||||
end
|
||||
|
||||
# Use Erlang's zip module to create a zip with many files
|
||||
{:ok, charlist_path} =
|
||||
:zip.create(
|
||||
String.to_charlist(zip_path),
|
||||
file_entries
|
||||
)
|
||||
|
||||
to_string(charlist_path)
|
||||
end
|
||||
|
||||
# Creates a zip with deeply nested directories
|
||||
defp create_deeply_nested_zip do
|
||||
zip_path = Path.join(@tmp_dir, "deep_nest.zip")
|
||||
|
||||
# Create a file to include in the zip
|
||||
file_content = "test content"
|
||||
|
||||
# Create a list of deeply nested files
|
||||
file_entries =
|
||||
for i <- 1..10 do
|
||||
nested_path = Enum.reduce(1..i, "nested", fn j, acc -> "#{acc}/level_#{j}" end)
|
||||
{String.to_charlist("#{nested_path}/file.txt"), file_content}
|
||||
end
|
||||
|
||||
# Use Erlang's zip module to create a zip with deeply nested directories
|
||||
{:ok, charlist_path} =
|
||||
:zip.create(
|
||||
String.to_charlist(zip_path),
|
||||
file_entries
|
||||
)
|
||||
|
||||
to_string(charlist_path)
|
||||
end
|
||||
end
|
||||
17
test/pleroma/schema_test.exs
Normal file
17
test/pleroma/schema_test.exs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.SchemaTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.Repo
|
||||
|
||||
test "No unindexed foreign keys" do
|
||||
query = File.read!("test/fixtures/unindexed_fk.sql")
|
||||
|
||||
{:ok, result} = Repo.query(query)
|
||||
|
||||
assert Enum.empty?(result.rows)
|
||||
end
|
||||
end
|
||||
|
|
@ -74,29 +74,6 @@ defmodule Pleroma.Search.MeilisearchTest do
|
|||
assert_received("posted_to_meilisearch")
|
||||
end
|
||||
|
||||
test "doesn't index posts that are not public" do
|
||||
user = insert(:user)
|
||||
|
||||
Enum.each(["private", "direct"], fn visibility ->
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
status: "guys i just don't wanna leave the swamp",
|
||||
visibility: visibility
|
||||
})
|
||||
|
||||
args = %{"op" => "add_to_index", "activity" => activity.id}
|
||||
|
||||
Config
|
||||
|> expect(:get, fn
|
||||
[Pleroma.Search, :module], nil ->
|
||||
Meilisearch
|
||||
end)
|
||||
|
||||
assert_enqueued(worker: SearchIndexingWorker, args: args)
|
||||
assert :ok = perform_job(SearchIndexingWorker, args)
|
||||
end)
|
||||
end
|
||||
|
||||
test "deletes posts from index when deleted locally" do
|
||||
user = insert(:user)
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ defmodule Pleroma.Search.QdrantSearchTest do
|
|||
})
|
||||
|
||||
Config
|
||||
|> expect(:get, 3, fn
|
||||
|> expect(:get, 4, fn
|
||||
[Pleroma.Search, :module], nil ->
|
||||
QdrantSearch
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ defmodule Pleroma.Search.QdrantSearchTest do
|
|||
})
|
||||
|
||||
Config
|
||||
|> expect(:get, 3, fn
|
||||
|> expect(:get, 4, fn
|
||||
[Pleroma.Search, :module], nil ->
|
||||
QdrantSearch
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ defmodule Pleroma.Search.QdrantSearchTest do
|
|||
end)
|
||||
|
||||
Config
|
||||
|> expect(:get, 6, fn
|
||||
|> expect(:get, 7, fn
|
||||
[Pleroma.Search, :module], nil ->
|
||||
QdrantSearch
|
||||
|
||||
|
|
|
|||
69
test/pleroma/search_test.exs
Normal file
69
test/pleroma/search_test.exs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.SearchTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
use Oban.Testing, repo: Pleroma.Repo
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Workers.SearchIndexingWorker
|
||||
|
||||
test "indexes posts that are public" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
status: "Well this is a story all about how my life got flipped turned upside down",
|
||||
visibility: "public"
|
||||
})
|
||||
|
||||
args = %{"op" => "add_to_index", "activity" => activity.id}
|
||||
|
||||
assert_enqueued(worker: SearchIndexingWorker, args: args)
|
||||
end
|
||||
|
||||
test "doesn't index posts that are not public" do
|
||||
user = insert(:user)
|
||||
|
||||
Enum.each(["private", "direct"], fn visibility ->
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
status: "guys i just don't wanna leave the swamp",
|
||||
visibility: visibility
|
||||
})
|
||||
|
||||
args = %{"op" => "add_to_index", "activity" => activity.id}
|
||||
|
||||
refute_enqueued(worker: SearchIndexingWorker, args: args)
|
||||
end)
|
||||
end
|
||||
|
||||
test "Indexes appropriate activity types" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
status: "I'm my own hype man",
|
||||
visibility: "public"
|
||||
})
|
||||
|
||||
args = %{"op" => "add_to_index", "activity" => activity.id}
|
||||
|
||||
assert_enqueued(worker: SearchIndexingWorker, args: args)
|
||||
|
||||
{:ok, fav_activity} = CommonAPI.favorite(activity.id, user)
|
||||
|
||||
args = %{"op" => "add_to_index", "activity" => fav_activity.id}
|
||||
|
||||
refute_enqueued(worker: SearchIndexingWorker, args: args)
|
||||
|
||||
{:ok, repeat_activity} = CommonAPI.repeat(activity.id, user)
|
||||
|
||||
args = %{"op" => "add_to_index", "activity" => repeat_activity.id}
|
||||
|
||||
refute_enqueued(worker: SearchIndexingWorker, args: args)
|
||||
end
|
||||
end
|
||||
|
|
@ -11,6 +11,7 @@ defmodule Pleroma.SignatureTest do
|
|||
import Mock
|
||||
|
||||
alias Pleroma.Signature
|
||||
alias Pleroma.StubbedHTTPSignaturesMock, as: HTTPSignaturesMock
|
||||
|
||||
setup do
|
||||
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||
|
|
@ -103,6 +104,18 @@ defmodule Pleroma.SignatureTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "validate_signature/1" do
|
||||
test "treats HTTP signature errors as failed validation" do
|
||||
conn = %Plug.Conn{method: "GET", request_path: "/inbox", req_headers: []}
|
||||
|
||||
Mox.expect(HTTPSignaturesMock, :validate_conn, fn _conn ->
|
||||
{:error, :request_target_header}
|
||||
end)
|
||||
|
||||
assert Signature.validate_signature(conn) == false
|
||||
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") ==
|
||||
|
|
|
|||
|
|
@ -73,8 +73,8 @@ defmodule Pleroma.StatsTest do
|
|||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
{:ok, activity} = CommonAPI.post(user, %{visibility: "public", status: "hey"})
|
||||
_ = CommonAPI.follow(user, other_user)
|
||||
CommonAPI.favorite(other_user, activity.id)
|
||||
_ = CommonAPI.follow(other_user, user)
|
||||
CommonAPI.favorite(activity.id, other_user)
|
||||
CommonAPI.repeat(activity.id, other_user)
|
||||
|
||||
assert %{"direct" => 0, "private" => 0, "public" => 1, "unlisted" => 0} =
|
||||
|
|
|
|||
99
test/pleroma/tesla/middleware/connection_pool_test.exs
Normal file
99
test/pleroma/tesla/middleware/connection_pool_test.exs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# 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.ConnectionPoolTest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
import Mox
|
||||
|
||||
alias Pleroma.Tesla.Middleware.ConnectionPool
|
||||
|
||||
defmodule FailingAdapter do
|
||||
@behaviour Tesla.Adapter
|
||||
|
||||
@impl Tesla.Adapter
|
||||
def call(_env, _opts), do: {:error, :request_failed}
|
||||
end
|
||||
|
||||
defmodule EmptyAdapter do
|
||||
@behaviour Tesla.Adapter
|
||||
|
||||
@impl Tesla.Adapter
|
||||
def call(env, _opts), do: {:ok, %{env | status: 204, body: ""}}
|
||||
end
|
||||
|
||||
test "discards the connection when the adapter cannot finish a request" do
|
||||
test_pid = self()
|
||||
|
||||
stub(Pleroma.GunMock, :open, fn _, _, _ ->
|
||||
{:ok, conn} = Task.start_link(fn -> Process.sleep(:infinity) end)
|
||||
send(test_pid, {:gun_conn, conn})
|
||||
{:ok, conn}
|
||||
end)
|
||||
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http} end)
|
||||
|
||||
client = Tesla.client([ConnectionPool], FailingAdapter)
|
||||
|
||||
assert {:error, :request_failed} =
|
||||
Tesla.get(client, "http://discard.example/", opts: [adapter: []])
|
||||
|
||||
assert_receive {:gun_conn, conn}
|
||||
|
||||
assert eventually(fn -> not Process.alive?(conn) end)
|
||||
end
|
||||
|
||||
test "releases a chunks lease when the response has no stream" do
|
||||
clear_config([:connections_pool, :max_idle_time], 5)
|
||||
test_pid = self()
|
||||
|
||||
stub(Pleroma.GunMock, :open, fn _, _, _ ->
|
||||
{:ok, conn} = Task.start_link(fn -> Process.sleep(:infinity) end)
|
||||
send(test_pid, {:gun_conn, conn})
|
||||
{:ok, conn}
|
||||
end)
|
||||
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http} end)
|
||||
|
||||
client = Tesla.client([ConnectionPool], EmptyAdapter)
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 204, body: ""}} =
|
||||
Tesla.get(client, "http://empty-chunks.example/",
|
||||
opts: [adapter: [body_as: :chunks]]
|
||||
)
|
||||
|
||||
assert_receive {:gun_conn, conn}
|
||||
worker = worker_for_conn(conn)
|
||||
assert eventually(fn -> not Process.alive?(worker) end)
|
||||
end
|
||||
|
||||
test "rejects unmanaged pooled streams" do
|
||||
client = Tesla.client([ConnectionPool], EmptyAdapter)
|
||||
|
||||
assert {:error, :pooled_streaming_requires_chunks} =
|
||||
Tesla.get(client, "http://stream.example/", opts: [adapter: [body_as: :stream]])
|
||||
end
|
||||
|
||||
defp worker_for_conn(conn) do
|
||||
[worker] =
|
||||
Registry.select(Pleroma.Gun.ConnectionPool, [
|
||||
{{:_, :"$1", {:"$2", :_}}, [{:==, :"$2", conn}], [:"$1"]}
|
||||
])
|
||||
|
||||
worker
|
||||
end
|
||||
|
||||
defp eventually(predicate, attempts \\ 50)
|
||||
|
||||
defp eventually(_predicate, 0), do: false
|
||||
|
||||
defp eventually(predicate, attempts) do
|
||||
if predicate.() do
|
||||
true
|
||||
else
|
||||
Process.sleep(10)
|
||||
eventually(predicate, attempts - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -34,6 +34,20 @@ defmodule Pleroma.Upload.Filter.AnalyzeMetadataTest do
|
|||
assert meta.blurhash == "eXJi-E:SwCEm5rCmn$+YWYn+15K#5A$xxCi{SiV]s*W:Efa#s.jE-T"
|
||||
end
|
||||
|
||||
test "it gets dimensions for grayscale images" do
|
||||
upload = %Pleroma.Upload{
|
||||
name: "break_analyze.png",
|
||||
content_type: "image/png",
|
||||
path: Path.absname("test/fixtures/break_analyze.png"),
|
||||
tempfile: Path.absname("test/fixtures/break_analyze.png")
|
||||
}
|
||||
|
||||
{:ok, :filtered, meta} = AnalyzeMetadata.filter(upload)
|
||||
|
||||
assert %{width: 1410, height: 2048} = meta
|
||||
assert is_nil(meta.blurhash)
|
||||
end
|
||||
|
||||
test "adds the dimensions for videos" do
|
||||
upload = %Pleroma.Upload{
|
||||
name: "coolvideo.mp4",
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do
|
||||
use Pleroma.DataCase
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
import Mox
|
||||
alias Pleroma.StaticStubbedConfigMock, as: ConfigMock
|
||||
alias Pleroma.Upload
|
||||
|
||||
setup do
|
||||
|
|
@ -19,21 +21,26 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do
|
|||
%{upload_file: upload_file}
|
||||
end
|
||||
|
||||
setup do: clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text])
|
||||
|
||||
test "it replaces filename on pre-defined text", %{upload_file: upload_file} do
|
||||
clear_config([Upload.Filter.AnonymizeFilename, :text], "custom-file.png")
|
||||
ConfigMock
|
||||
|> stub(:get, fn [Upload.Filter.AnonymizeFilename, :text] -> "custom-file.png" end)
|
||||
|
||||
{:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file)
|
||||
assert name == "custom-file.png"
|
||||
end
|
||||
|
||||
test "it replaces filename on pre-defined text expression", %{upload_file: upload_file} do
|
||||
clear_config([Upload.Filter.AnonymizeFilename, :text], "custom-file.{extension}")
|
||||
ConfigMock
|
||||
|> stub(:get, fn [Upload.Filter.AnonymizeFilename, :text] -> "custom-file.{extension}" end)
|
||||
|
||||
{:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file)
|
||||
assert name == "custom-file.jpg"
|
||||
end
|
||||
|
||||
test "it replaces filename on random text", %{upload_file: upload_file} do
|
||||
ConfigMock
|
||||
|> stub(:get, fn [Upload.Filter.AnonymizeFilename, :text] -> nil end)
|
||||
|
||||
{:ok, :filtered, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file)
|
||||
assert <<_::bytes-size(14)>> <> ".jpg" = name
|
||||
refute name == "an… image.jpg"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ defmodule Pleroma.Upload.Filter.DedupeTest do
|
|||
|
||||
@shasum "e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781"
|
||||
|
||||
test "generates a shard path for a shasum" do
|
||||
assert "e3/03/97/" <> _path = Dedupe.shard_path(@shasum)
|
||||
end
|
||||
|
||||
test "adds shasum" do
|
||||
File.cp!(
|
||||
"test/fixtures/image.jpg",
|
||||
|
|
@ -23,10 +27,12 @@ defmodule Pleroma.Upload.Filter.DedupeTest do
|
|||
tempfile: Path.absname("test/fixtures/image_tmp.jpg")
|
||||
}
|
||||
|
||||
expected_path = Dedupe.shard_path(@shasum <> ".jpg")
|
||||
|
||||
assert {
|
||||
:ok,
|
||||
:filtered,
|
||||
%Pleroma.Upload{id: @shasum, path: @shasum <> ".jpg"}
|
||||
%Pleroma.Upload{id: @shasum, path: ^expected_path}
|
||||
} = Dedupe.filter(upload)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,29 +9,31 @@ defmodule Pleroma.Upload.Filter.Exiftool.StripLocationTest do
|
|||
test "apply exiftool filter" do
|
||||
assert Pleroma.Utils.command_available?("exiftool")
|
||||
|
||||
File.cp!(
|
||||
"test/fixtures/DSCN0010.jpg",
|
||||
"test/fixtures/DSCN0010_tmp.jpg"
|
||||
)
|
||||
~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.jpg",
|
||||
content_type: "image/jpeg",
|
||||
path: Path.absname("test/fixtures/DSCN0010.jpg"),
|
||||
tempfile: Path.absname("test/fixtures/DSCN0010_tmp.jpg")
|
||||
}
|
||||
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}
|
||||
assert Filter.Exiftool.StripLocation.filter(upload) == {:ok, :filtered}
|
||||
|
||||
{exif_original, 0} = System.cmd("exiftool", ["test/fixtures/DSCN0010.jpg"])
|
||||
{exif_filtered, 0} = System.cmd("exiftool", ["test/fixtures/DSCN0010_tmp.jpg"])
|
||||
{exif_original, 0} = System.cmd("exiftool", ["-m", "test/fixtures/DSCN0010.#{type}"])
|
||||
{exif_filtered, 0} = System.cmd("exiftool", ["-m", "test/fixtures/DSCN0010_tmp.#{type}"])
|
||||
|
||||
refute exif_original == exif_filtered
|
||||
assert String.match?(exif_original, ~r/GPS/)
|
||||
refute String.match?(exif_filtered, ~r/GPS/)
|
||||
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
|
||||
test "verify webp, heic, svg files are skipped" do
|
||||
uploads =
|
||||
~w{webp heic svg svg+xml}
|
||||
|> Enum.map(fn type ->
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Upload.Filter.MogrifunTest do
|
||||
use Pleroma.DataCase
|
||||
import Mock
|
||||
use Pleroma.DataCase, async: true
|
||||
import Mox
|
||||
|
||||
alias Pleroma.MogrifyMock
|
||||
alias Pleroma.Upload
|
||||
alias Pleroma.Upload.Filter
|
||||
|
||||
|
|
@ -22,23 +23,12 @@ defmodule Pleroma.Upload.Filter.MogrifunTest do
|
|||
tempfile: Path.absname("test/fixtures/image_tmp.jpg")
|
||||
}
|
||||
|
||||
task =
|
||||
Task.async(fn ->
|
||||
assert_receive {:apply_filter, {}}, 4_000
|
||||
end)
|
||||
MogrifyMock
|
||||
|> stub(:open, fn _file -> %{} end)
|
||||
|> stub(:custom, fn _image, _action -> %{} end)
|
||||
|> stub(:custom, fn _image, _action, _options -> %{} end)
|
||||
|> stub(:save, fn _image, [in_place: true] -> :ok end)
|
||||
|
||||
with_mocks([
|
||||
{Mogrify, [],
|
||||
[
|
||||
open: fn _f -> %Mogrify.Image{} end,
|
||||
custom: fn _m, _a -> send(task.pid, {:apply_filter, {}}) end,
|
||||
custom: fn _m, _a, _o -> send(task.pid, {:apply_filter, {}}) end,
|
||||
save: fn _f, _o -> :ok end
|
||||
]}
|
||||
]) do
|
||||
assert Filter.Mogrifun.filter(upload) == {:ok, :filtered}
|
||||
end
|
||||
|
||||
Task.await(task)
|
||||
assert Filter.Mogrifun.filter(upload) == {:ok, :filtered}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,13 +3,18 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Upload.Filter.MogrifyTest do
|
||||
use Pleroma.DataCase
|
||||
import Mock
|
||||
use Pleroma.DataCase, async: true
|
||||
import Mox
|
||||
|
||||
alias Pleroma.MogrifyMock
|
||||
alias Pleroma.StaticStubbedConfigMock, as: ConfigMock
|
||||
alias Pleroma.Upload.Filter
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
test "apply mogrify filter" do
|
||||
clear_config(Filter.Mogrify, args: [{"tint", "40"}])
|
||||
ConfigMock
|
||||
|> stub(:get!, fn [Filter.Mogrify, :args] -> [{"tint", "40"}] end)
|
||||
|
||||
File.cp!(
|
||||
"test/fixtures/image.jpg",
|
||||
|
|
@ -23,19 +28,11 @@ defmodule Pleroma.Upload.Filter.MogrifyTest do
|
|||
tempfile: Path.absname("test/fixtures/image_tmp.jpg")
|
||||
}
|
||||
|
||||
task =
|
||||
Task.async(fn ->
|
||||
assert_receive {:apply_filter, {_, "tint", "40"}}, 4_000
|
||||
end)
|
||||
MogrifyMock
|
||||
|> expect(:open, fn _file -> %{} end)
|
||||
|> expect(:custom, fn _image, "tint", "40" -> %{} end)
|
||||
|> expect(:save, fn _image, [in_place: true] -> :ok end)
|
||||
|
||||
with_mock Mogrify,
|
||||
open: fn _f -> %Mogrify.Image{} end,
|
||||
custom: fn _m, _a -> :ok end,
|
||||
custom: fn m, a, o -> send(task.pid, {:apply_filter, {m, a, o}}) end,
|
||||
save: fn _f, _o -> :ok end do
|
||||
assert Filter.Mogrify.filter(upload) == {:ok, :filtered}
|
||||
end
|
||||
|
||||
Task.await(task)
|
||||
assert Filter.Mogrify.filter(upload) == {:ok, :filtered}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@
|
|||
defmodule Pleroma.Upload.FilterTest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
import Mox
|
||||
alias Pleroma.StaticStubbedConfigMock, as: ConfigMock
|
||||
alias Pleroma.Upload.Filter
|
||||
|
||||
setup do: clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text])
|
||||
|
||||
test "applies filters" do
|
||||
clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text], "custom-file.png")
|
||||
ConfigMock
|
||||
|> stub(:get, fn [Pleroma.Upload.Filter.AnonymizeFilename, :text] -> "custom-file.png" end)
|
||||
|
||||
File.cp!(
|
||||
"test/fixtures/image.jpg",
|
||||
|
|
|
|||
|
|
@ -149,6 +149,9 @@ defmodule Pleroma.UploadTest do
|
|||
|
||||
test "copies the file to the configured folder with deduping" do
|
||||
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
|
||||
expected_filename = "e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781.jpg"
|
||||
|
||||
expected_path = Pleroma.Upload.Filter.Dedupe.shard_path(expected_filename)
|
||||
|
||||
file = %Plug.Upload{
|
||||
content_type: "image/jpeg",
|
||||
|
|
@ -159,8 +162,7 @@ defmodule Pleroma.UploadTest do
|
|||
{:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.Dedupe])
|
||||
|
||||
assert List.first(data["url"])["href"] ==
|
||||
Pleroma.Upload.base_url() <>
|
||||
"e30397b58d226d6583ab5b8b3c5defb0c682bda5c31ef07a9f57c1c4986e3781.jpg"
|
||||
Path.join([Pleroma.Upload.base_url(), expected_path])
|
||||
end
|
||||
|
||||
test "copies the file to the configured folder without deduping" do
|
||||
|
|
@ -225,20 +227,35 @@ defmodule Pleroma.UploadTest do
|
|||
assert Path.basename(attachment_url["href"]) == "an%E2%80%A6%20image.jpg"
|
||||
end
|
||||
|
||||
test "escapes reserved uri characters" do
|
||||
test "escapes disallowed reserved characters in uri path" do
|
||||
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
|
||||
|
||||
file = %Plug.Upload{
|
||||
content_type: "image/jpeg",
|
||||
path: Path.absname("test/fixtures/image_tmp.jpg"),
|
||||
filename: ":?#[]@!$&\\'()*+,;=.jpg"
|
||||
filename: ":?#[]@!$&'()*+,;=.jpg"
|
||||
}
|
||||
|
||||
{:ok, data} = Upload.store(file)
|
||||
[attachment_url | _] = data["url"]
|
||||
|
||||
assert Path.basename(attachment_url["href"]) ==
|
||||
"%3A%3F%23%5B%5D%40%21%24%26%5C%27%28%29%2A%2B%2C%3B%3D.jpg"
|
||||
":%3F%23%5B%5D@!$&'()*+,;=.jpg"
|
||||
end
|
||||
|
||||
test "double %-encodes filename" do
|
||||
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
|
||||
|
||||
file = %Plug.Upload{
|
||||
content_type: "image/jpeg",
|
||||
path: Path.absname("test/fixtures/image_tmp.jpg"),
|
||||
filename: "file with %20.jpg"
|
||||
}
|
||||
|
||||
{:ok, data} = Upload.store(file)
|
||||
[attachment_url | _] = data["url"]
|
||||
|
||||
assert Path.basename(attachment_url["href"]) == "file%20with%20%2520.jpg"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -265,4 +282,23 @@ defmodule Pleroma.UploadTest do
|
|||
refute String.starts_with?(url, base_url <> "/media/")
|
||||
end
|
||||
end
|
||||
|
||||
describe "Setting a link_name for uploaded media" do
|
||||
setup do: clear_config([Pleroma.Upload, :link_name], true)
|
||||
|
||||
test "encodes name parameter in query" do
|
||||
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
|
||||
|
||||
file = %Plug.Upload{
|
||||
content_type: "image/jpeg",
|
||||
path: Path.absname("test/fixtures/image_tmp.jpg"),
|
||||
filename: "test file.jpg"
|
||||
}
|
||||
|
||||
{:ok, data} = Upload.store(file)
|
||||
[attachment_url | _] = data["url"]
|
||||
|
||||
assert Path.basename(attachment_url["href"]) == "test%20file.jpg?name=test+file.jpg"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,25 +14,6 @@ defmodule Pleroma.Uploaders.IPFSTest do
|
|||
|
||||
alias Pleroma.UnstubbedConfigMock, as: Config
|
||||
|
||||
describe "get_final_url" do
|
||||
setup do
|
||||
Config
|
||||
|> expect(:get, fn [Pleroma.Uploaders.IPFS] ->
|
||||
[post_gateway_url: "http://localhost:5001"]
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "it returns the final url for put_file" do
|
||||
assert IPFS.put_file_endpoint() == "http://localhost:5001/api/v0/add"
|
||||
end
|
||||
|
||||
test "it returns the final url for delete_file" do
|
||||
assert IPFS.delete_file_endpoint() == "http://localhost:5001/api/v0/files/rm"
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_file/1" do
|
||||
setup do
|
||||
Config
|
||||
|
|
@ -71,8 +52,8 @@ defmodule Pleroma.Uploaders.IPFSTest do
|
|||
describe "put_file/1" do
|
||||
setup do
|
||||
Config
|
||||
|> expect(:get, fn [Pleroma.Uploaders.IPFS] ->
|
||||
[post_gateway_url: "http://localhost:5001"]
|
||||
|> expect(:get, fn [Pleroma.Uploaders.IPFS, :post_gateway_url] ->
|
||||
"http://localhost:5001"
|
||||
end)
|
||||
|
||||
file_upload = %Pleroma.Upload{
|
||||
|
|
@ -92,7 +73,11 @@ defmodule Pleroma.Uploaders.IPFSTest do
|
|||
|
||||
test "save file", %{file_upload: file_upload} do
|
||||
with_mock Pleroma.HTTP,
|
||||
post: fn "http://localhost:5001/api/v0/add", _mp, [], params: ["cid-version": "1"] ->
|
||||
post: fn "http://localhost:5001/api/v0/add",
|
||||
_mp,
|
||||
[],
|
||||
params: ["cid-version": "1"],
|
||||
pool: :upload ->
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
|
|
@ -107,7 +92,11 @@ defmodule Pleroma.Uploaders.IPFSTest do
|
|||
|
||||
test "returns error", %{file_upload: file_upload} do
|
||||
with_mock Pleroma.HTTP,
|
||||
post: fn "http://localhost:5001/api/v0/add", _mp, [], params: ["cid-version": "1"] ->
|
||||
post: fn "http://localhost:5001/api/v0/add",
|
||||
_mp,
|
||||
[],
|
||||
params: ["cid-version": "1"],
|
||||
pool: :upload ->
|
||||
{:error, "IPFS Gateway upload failed"}
|
||||
end do
|
||||
assert capture_log(fn ->
|
||||
|
|
@ -118,7 +107,11 @@ defmodule Pleroma.Uploaders.IPFSTest do
|
|||
|
||||
test "returns error if JSON decode fails", %{file_upload: file_upload} do
|
||||
with_mock Pleroma.HTTP, [],
|
||||
post: fn "http://localhost:5001/api/v0/add", _mp, [], params: ["cid-version": "1"] ->
|
||||
post: fn "http://localhost:5001/api/v0/add",
|
||||
_mp,
|
||||
[],
|
||||
params: ["cid-version": "1"],
|
||||
pool: :upload ->
|
||||
{:ok, %Tesla.Env{status: 200, body: "invalid"}}
|
||||
end do
|
||||
assert capture_log(fn ->
|
||||
|
|
@ -130,7 +123,11 @@ defmodule Pleroma.Uploaders.IPFSTest do
|
|||
|
||||
test "returns error if JSON body doesn't contain Hash key", %{file_upload: file_upload} do
|
||||
with_mock Pleroma.HTTP, [],
|
||||
post: fn "http://localhost:5001/api/v0/add", _mp, [], params: ["cid-version": "1"] ->
|
||||
post: fn "http://localhost:5001/api/v0/add",
|
||||
_mp,
|
||||
[],
|
||||
params: ["cid-version": "1"],
|
||||
pool: :upload ->
|
||||
{:ok, %Tesla.Env{status: 200, body: "{\"key\": \"value\"}"}}
|
||||
end do
|
||||
assert IPFS.put_file(file_upload) == {:error, "JSON doesn't contain Hash key"}
|
||||
|
|
@ -141,8 +138,8 @@ defmodule Pleroma.Uploaders.IPFSTest do
|
|||
describe "delete_file/1" do
|
||||
setup do
|
||||
Config
|
||||
|> expect(:get, fn [Pleroma.Uploaders.IPFS] ->
|
||||
[post_gateway_url: "http://localhost:5001"]
|
||||
|> expect(:get, fn [Pleroma.Uploaders.IPFS, :post_gateway_url] ->
|
||||
"http://localhost:5001"
|
||||
end)
|
||||
|
||||
:ok
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.User.BackupAsyncTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
import Pleroma.Factory
|
||||
import Mox
|
||||
|
||||
alias Pleroma.UnstubbedConfigMock, as: ConfigMock
|
||||
alias Pleroma.User.Backup
|
||||
alias Pleroma.User.Backup.ProcessorMock
|
||||
|
||||
setup do
|
||||
user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"})
|
||||
|
||||
{:ok, backup} = user |> Backup.new() |> Repo.insert()
|
||||
%{backup: backup}
|
||||
end
|
||||
|
||||
@tag capture_log: true
|
||||
test "it handles unrecoverable exceptions", %{backup: backup} do
|
||||
ProcessorMock
|
||||
|> expect(:do_process, fn _, _ ->
|
||||
raise "mock exception"
|
||||
end)
|
||||
|
||||
ConfigMock
|
||||
|> stub_with(Pleroma.Config)
|
||||
|
||||
{:error, %{backup: backup, reason: :exit}} = Backup.process(backup, ProcessorMock)
|
||||
|
||||
assert backup.state == :failed
|
||||
end
|
||||
|
||||
@tag capture_log: true
|
||||
test "it handles timeouts", %{backup: backup} do
|
||||
ProcessorMock
|
||||
|> expect(:do_process, fn _, _ ->
|
||||
Process.sleep(:timer.seconds(4))
|
||||
end)
|
||||
|
||||
ConfigMock
|
||||
|> expect(:get, fn [Pleroma.User.Backup, :process_wait_time] -> :timer.seconds(2) end)
|
||||
|
||||
{:error, %{backup: backup, reason: :timeout}} = Backup.process(backup, ProcessorMock)
|
||||
|
||||
assert backup.state == :failed
|
||||
end
|
||||
end
|
||||
|
|
@ -6,7 +6,6 @@ defmodule Pleroma.User.BackupTest do
|
|||
use Oban.Testing, repo: Pleroma.Repo
|
||||
use Pleroma.DataCase
|
||||
|
||||
import Mock
|
||||
import Pleroma.Factory
|
||||
import Swoosh.TestAssertions
|
||||
import Mox
|
||||
|
|
@ -16,7 +15,6 @@ defmodule Pleroma.User.BackupTest do
|
|||
alias Pleroma.UnstubbedConfigMock, as: ConfigMock
|
||||
alias Pleroma.Uploaders.S3.ExAwsMock
|
||||
alias Pleroma.User.Backup
|
||||
alias Pleroma.User.Backup.ProcessorMock
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Workers.BackupWorker
|
||||
|
||||
|
|
@ -28,79 +26,56 @@ defmodule Pleroma.User.BackupTest do
|
|||
ConfigMock
|
||||
|> stub_with(Pleroma.Config)
|
||||
|
||||
ProcessorMock
|
||||
|> stub_with(Pleroma.User.Backup.Processor)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "it does not requrie enabled email" do
|
||||
clear_config([Pleroma.Emails.Mailer, :enabled], false)
|
||||
user = insert(:user)
|
||||
assert {:ok, _} = Backup.create(user)
|
||||
assert {:ok, _} = Backup.user(user)
|
||||
end
|
||||
|
||||
test "it does not require user's email" do
|
||||
user = insert(:user, %{email: nil})
|
||||
assert {:ok, _} = Backup.create(user)
|
||||
assert {:ok, _} = Backup.user(user)
|
||||
end
|
||||
|
||||
test "it creates a backup record and an Oban job" do
|
||||
%{id: user_id} = user = insert(:user)
|
||||
assert {:ok, %Oban.Job{args: args}} = Backup.create(user)
|
||||
user = insert(:user)
|
||||
assert {:ok, %Backup{} = backup} = Backup.user(user)
|
||||
assert {:ok, %Oban.Job{args: args}} = Backup.schedule_backup(backup)
|
||||
assert_enqueued(worker: BackupWorker, args: args)
|
||||
|
||||
backup = Backup.get(args["backup_id"])
|
||||
assert %Backup{user_id: ^user_id, processed: false, file_size: 0, state: :pending} = backup
|
||||
backup = Backup.get_by_id(args["backup_id"])
|
||||
assert %Backup{processed: false, file_size: 0} = backup
|
||||
end
|
||||
|
||||
test "it return an error if the export limit is over" do
|
||||
%{id: user_id} = user = insert(:user)
|
||||
user = insert(:user)
|
||||
limit_days = Pleroma.Config.get([Backup, :limit_days])
|
||||
assert {:ok, %Oban.Job{args: args}} = Backup.create(user)
|
||||
backup = Backup.get(args["backup_id"])
|
||||
assert %Backup{user_id: ^user_id, processed: false, file_size: 0} = backup
|
||||
{:ok, first_backup} = Backup.user(user)
|
||||
{:ok, _run_backup} = Backup.run(first_backup)
|
||||
|
||||
assert Backup.create(user) == {:error, "Last export was less than #{limit_days} days ago"}
|
||||
assert Backup.user(user) == {:error, "Last export was less than #{limit_days} days ago"}
|
||||
end
|
||||
|
||||
test "it process a backup record" do
|
||||
clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
|
||||
%{id: user_id} = user = insert(:user)
|
||||
|
||||
assert {:ok, %Oban.Job{args: %{"backup_id" => backup_id} = args}} = Backup.create(user)
|
||||
assert {:ok, backup} = perform_job(BackupWorker, args)
|
||||
assert {:ok, %Backup{id: backup_id}} = Backup.user(user)
|
||||
|
||||
oban_args = %{"op" => "process", "backup_id" => backup_id}
|
||||
|
||||
assert {:ok, backup} = perform_job(BackupWorker, oban_args)
|
||||
assert backup.file_size > 0
|
||||
assert %Backup{id: ^backup_id, processed: true, user_id: ^user_id, state: :complete} = backup
|
||||
assert match?(%Backup{id: ^backup_id, processed: true, user_id: ^user_id}, backup)
|
||||
|
||||
delete_job_args = %{"op" => "delete", "backup_id" => backup_id}
|
||||
|
||||
assert_enqueued(worker: BackupWorker, args: delete_job_args)
|
||||
assert {:ok, backup} = perform_job(BackupWorker, delete_job_args)
|
||||
refute Backup.get(backup_id)
|
||||
|
||||
email = Pleroma.Emails.UserEmail.backup_is_ready_email(backup)
|
||||
|
||||
assert_email_sent(
|
||||
to: {user.name, user.email},
|
||||
html_body: email.html_body
|
||||
)
|
||||
end
|
||||
|
||||
test "it updates states of the backup" do
|
||||
clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
|
||||
%{id: user_id} = user = insert(:user)
|
||||
|
||||
assert {:ok, %Oban.Job{args: %{"backup_id" => backup_id} = args}} = Backup.create(user)
|
||||
assert {:ok, backup} = perform_job(BackupWorker, args)
|
||||
assert backup.file_size > 0
|
||||
assert %Backup{id: ^backup_id, processed: true, user_id: ^user_id, state: :complete} = backup
|
||||
|
||||
delete_job_args = %{"op" => "delete", "backup_id" => backup_id}
|
||||
|
||||
assert_enqueued(worker: BackupWorker, args: delete_job_args)
|
||||
assert {:ok, backup} = perform_job(BackupWorker, delete_job_args)
|
||||
refute Backup.get(backup_id)
|
||||
refute Backup.get_by_id(backup_id)
|
||||
|
||||
email = Pleroma.Emails.UserEmail.backup_is_ready_email(backup)
|
||||
|
||||
|
|
@ -114,10 +89,15 @@ defmodule Pleroma.User.BackupTest do
|
|||
clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
|
||||
%{id: user_id} = user = insert(:user, %{email: nil})
|
||||
|
||||
assert {:ok, %Oban.Job{args: %{"backup_id" => backup_id} = args}} = Backup.create(user)
|
||||
assert {:ok, backup} = perform_job(BackupWorker, args)
|
||||
assert backup.file_size > 0
|
||||
assert %Backup{id: ^backup_id, processed: true, user_id: ^user_id} = backup
|
||||
assert {:ok, %Backup{} = backup} = Backup.user(user)
|
||||
|
||||
expected_args = %{"op" => "process", "backup_id" => backup.id}
|
||||
|
||||
assert_enqueued(worker: BackupWorker, args: %{"backup_id" => backup.id})
|
||||
assert {:ok, completed_backup} = perform_job(BackupWorker, expected_args)
|
||||
assert completed_backup.file_size > 0
|
||||
assert completed_backup.processed
|
||||
assert completed_backup.user_id == user_id
|
||||
|
||||
assert_no_email_sent()
|
||||
end
|
||||
|
|
@ -127,10 +107,13 @@ defmodule Pleroma.User.BackupTest do
|
|||
clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
|
||||
%{id: user_id} = user = insert(:user)
|
||||
|
||||
assert {:ok, %Oban.Job{args: %{"backup_id" => backup_id} = args}} = Backup.create(user)
|
||||
assert {:ok, backup} = perform_job(BackupWorker, args)
|
||||
assert {:ok, %Backup{id: backup_id}} = Backup.user(user)
|
||||
|
||||
oban_args = %{"op" => "process", "backup_id" => backup_id}
|
||||
|
||||
assert {:ok, backup} = perform_job(BackupWorker, oban_args)
|
||||
assert backup.file_size > 0
|
||||
assert %Backup{id: ^backup_id, processed: true, user_id: ^user_id} = backup
|
||||
assert match?(%Backup{id: ^backup_id, processed: true, user_id: ^user_id}, backup)
|
||||
|
||||
assert_no_email_sent()
|
||||
end
|
||||
|
|
@ -139,10 +122,15 @@ defmodule Pleroma.User.BackupTest do
|
|||
clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
|
||||
%{id: user_id} = user = insert(:user, %{email: ""})
|
||||
|
||||
assert {:ok, %Oban.Job{args: %{"backup_id" => backup_id} = args}} = Backup.create(user)
|
||||
assert {:ok, backup} = perform_job(BackupWorker, args)
|
||||
assert {:ok, %Backup{id: backup_id} = backup} = Backup.user(user)
|
||||
|
||||
expected_args = %{"op" => "process", "backup_id" => backup.id}
|
||||
|
||||
assert_enqueued(worker: BackupWorker, args: expected_args)
|
||||
|
||||
assert {:ok, backup} = perform_job(BackupWorker, expected_args)
|
||||
assert backup.file_size > 0
|
||||
assert %Backup{id: ^backup_id, processed: true, user_id: ^user_id} = backup
|
||||
assert match?(%Backup{id: ^backup_id, processed: true, user_id: ^user_id}, backup)
|
||||
|
||||
assert_no_email_sent()
|
||||
end
|
||||
|
|
@ -152,16 +140,13 @@ defmodule Pleroma.User.BackupTest do
|
|||
clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
|
||||
user = insert(:user)
|
||||
|
||||
assert {:ok, job1} = Backup.create(user)
|
||||
|
||||
assert {:ok, %Backup{}} = ObanHelpers.perform(job1)
|
||||
assert {:ok, job2} = Backup.create(user)
|
||||
assert Pleroma.Repo.aggregate(Backup, :count) == 2
|
||||
assert {:ok, backup2} = ObanHelpers.perform(job2)
|
||||
assert {:ok, %{id: backup_one_id}} = Backup.user(user)
|
||||
assert {:ok, %{id: _backup_two_id}} = Backup.user(user)
|
||||
|
||||
# Run the backups
|
||||
ObanHelpers.perform_all()
|
||||
|
||||
assert [^backup2] = Pleroma.Repo.all(Backup)
|
||||
assert_enqueued(worker: BackupWorker, args: %{"op" => "delete", "backup_id" => backup_one_id})
|
||||
end
|
||||
|
||||
test "it creates a zip archive with user data" do
|
||||
|
|
@ -177,18 +162,21 @@ defmodule Pleroma.User.BackupTest do
|
|||
{:ok, %{object: %{data: %{"id" => id3}}} = status3} =
|
||||
CommonAPI.post(user, %{status: "status3"})
|
||||
|
||||
CommonAPI.favorite(user, status1.id)
|
||||
CommonAPI.favorite(user, status2.id)
|
||||
CommonAPI.favorite(status1.id, user)
|
||||
CommonAPI.favorite(status2.id, user)
|
||||
|
||||
Bookmark.create(user.id, status2.id)
|
||||
Bookmark.create(user.id, status3.id)
|
||||
|
||||
CommonAPI.follow(user, other_user)
|
||||
CommonAPI.follow(other_user, user)
|
||||
|
||||
assert {:ok, backup} = user |> Backup.new() |> Repo.insert()
|
||||
assert {:ok, path} = Backup.export(backup, self())
|
||||
assert {:ok, zipfile} = :zip.zip_open(String.to_charlist(path), [:memory])
|
||||
assert {:ok, {'actor.json', json}} = :zip.zip_get('actor.json', zipfile)
|
||||
assert {:ok, backup} = Backup.user(user)
|
||||
assert {:ok, run_backup} = Backup.run(backup)
|
||||
|
||||
tempfile = Path.join([run_backup.tempdir, run_backup.file_name])
|
||||
|
||||
assert {:ok, zipfile} = :zip.zip_open(String.to_charlist(tempfile), [:memory])
|
||||
assert {:ok, {~c"actor.json", json}} = :zip.zip_get(~c"actor.json", zipfile)
|
||||
|
||||
assert %{
|
||||
"@context" => [
|
||||
|
|
@ -197,13 +185,13 @@ defmodule Pleroma.User.BackupTest do
|
|||
%{"@language" => "und"}
|
||||
],
|
||||
"bookmarks" => "bookmarks.json",
|
||||
"followers" => "http://cofe.io/users/cofe/followers",
|
||||
"following" => "http://cofe.io/users/cofe/following",
|
||||
"followers" => "followers.json",
|
||||
"following" => "following.json",
|
||||
"id" => "http://cofe.io/users/cofe",
|
||||
"inbox" => "http://cofe.io/users/cofe/inbox",
|
||||
"likes" => "likes.json",
|
||||
"name" => "Cofe",
|
||||
"outbox" => "http://cofe.io/users/cofe/outbox",
|
||||
"outbox" => "outbox.json",
|
||||
"preferredUsername" => "cofe",
|
||||
"publicKey" => %{
|
||||
"id" => "http://cofe.io/users/cofe#main-key",
|
||||
|
|
@ -213,7 +201,7 @@ defmodule Pleroma.User.BackupTest do
|
|||
"url" => "http://cofe.io/users/cofe"
|
||||
} = Jason.decode!(json)
|
||||
|
||||
assert {:ok, {'outbox.json', json}} = :zip.zip_get('outbox.json', zipfile)
|
||||
assert {:ok, {~c"outbox.json", json}} = :zip.zip_get(~c"outbox.json", zipfile)
|
||||
|
||||
assert %{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
|
|
@ -244,7 +232,7 @@ defmodule Pleroma.User.BackupTest do
|
|||
"type" => "OrderedCollection"
|
||||
} = Jason.decode!(json)
|
||||
|
||||
assert {:ok, {'likes.json', json}} = :zip.zip_get('likes.json', zipfile)
|
||||
assert {:ok, {~c"likes.json", json}} = :zip.zip_get(~c"likes.json", zipfile)
|
||||
|
||||
assert %{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
|
|
@ -254,7 +242,7 @@ defmodule Pleroma.User.BackupTest do
|
|||
"type" => "OrderedCollection"
|
||||
} = Jason.decode!(json)
|
||||
|
||||
assert {:ok, {'bookmarks.json', json}} = :zip.zip_get('bookmarks.json', zipfile)
|
||||
assert {:ok, {~c"bookmarks.json", json}} = :zip.zip_get(~c"bookmarks.json", zipfile)
|
||||
|
||||
assert %{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
|
|
@ -264,7 +252,7 @@ defmodule Pleroma.User.BackupTest do
|
|||
"type" => "OrderedCollection"
|
||||
} = Jason.decode!(json)
|
||||
|
||||
assert {:ok, {'following.json', json}} = :zip.zip_get('following.json', zipfile)
|
||||
assert {:ok, {~c"following.json", json}} = :zip.zip_get(~c"following.json", zipfile)
|
||||
|
||||
assert %{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
|
|
@ -275,56 +263,34 @@ defmodule Pleroma.User.BackupTest do
|
|||
} = Jason.decode!(json)
|
||||
|
||||
:zip.zip_close(zipfile)
|
||||
File.rm!(path)
|
||||
File.rm_rf!(run_backup.tempdir)
|
||||
end
|
||||
|
||||
test "it counts the correct number processed" do
|
||||
test "correct number processed" do
|
||||
user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"})
|
||||
|
||||
Enum.map(1..120, fn i ->
|
||||
{:ok, status} = CommonAPI.post(user, %{status: "status #{i}"})
|
||||
CommonAPI.favorite(user, status.id)
|
||||
CommonAPI.favorite(status.id, user)
|
||||
Bookmark.create(user.id, status.id)
|
||||
end)
|
||||
|
||||
assert {:ok, backup} = user |> Backup.new() |> Repo.insert()
|
||||
{:ok, backup} = Backup.process(backup)
|
||||
{:ok, backup} = Backup.run(backup)
|
||||
|
||||
assert backup.processed_number == 1 + 120 + 120 + 120
|
||||
zip_path = Path.join([backup.tempdir, backup.file_name])
|
||||
|
||||
Backup.delete(backup)
|
||||
end
|
||||
assert {:ok, zipfile} = :zip.zip_open(String.to_charlist(zip_path), [:memory])
|
||||
|
||||
test "it handles errors" do
|
||||
user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"})
|
||||
backup_parts = [~c"likes.json", ~c"bookmarks.json", ~c"outbox.json"]
|
||||
|
||||
Enum.map(1..120, fn i ->
|
||||
{:ok, _status} = CommonAPI.post(user, %{status: "status #{i}"})
|
||||
Enum.each(backup_parts, fn part ->
|
||||
assert {:ok, {_part, part_json}} = :zip.zip_get(part, zipfile)
|
||||
{:ok, decoded_part} = Jason.decode(part_json)
|
||||
assert decoded_part["totalItems"] == 120
|
||||
end)
|
||||
|
||||
assert {:ok, backup} = user |> Backup.new() |> Repo.insert()
|
||||
|
||||
with_mock Pleroma.Web.ActivityPub.Transmogrifier,
|
||||
[:passthrough],
|
||||
prepare_outgoing: fn data ->
|
||||
object =
|
||||
data["object"]
|
||||
|> Pleroma.Object.normalize(fetch: false)
|
||||
|> Map.get(:data)
|
||||
|
||||
data = data |> Map.put("object", object)
|
||||
|
||||
if String.contains?(data["object"]["content"], "119"),
|
||||
do: raise(%Postgrex.Error{}),
|
||||
else: {:ok, data}
|
||||
end do
|
||||
{:ok, backup} = Backup.process(backup)
|
||||
assert backup.processed
|
||||
assert backup.state == :complete
|
||||
assert backup.processed_number == 1 + 119
|
||||
|
||||
Backup.delete(backup)
|
||||
end
|
||||
Backup.delete_archive(backup)
|
||||
end
|
||||
|
||||
describe "it uploads and deletes a backup archive" do
|
||||
|
|
@ -337,18 +303,17 @@ defmodule Pleroma.User.BackupTest do
|
|||
{:ok, status1} = CommonAPI.post(user, %{status: "status1"})
|
||||
{:ok, status2} = CommonAPI.post(user, %{status: "status2"})
|
||||
{:ok, status3} = CommonAPI.post(user, %{status: "status3"})
|
||||
CommonAPI.favorite(user, status1.id)
|
||||
CommonAPI.favorite(user, status2.id)
|
||||
CommonAPI.favorite(status1.id, user)
|
||||
CommonAPI.favorite(status2.id, user)
|
||||
Bookmark.create(user.id, status2.id)
|
||||
Bookmark.create(user.id, status3.id)
|
||||
|
||||
assert {:ok, backup} = user |> Backup.new() |> Repo.insert()
|
||||
assert {:ok, path} = Backup.export(backup, self())
|
||||
|
||||
[path: path, backup: backup]
|
||||
[backup: backup]
|
||||
end
|
||||
|
||||
test "S3", %{path: path, backup: backup} do
|
||||
test "S3", %{backup: backup} do
|
||||
clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.S3)
|
||||
clear_config([Pleroma.Uploaders.S3, :streaming_enabled], false)
|
||||
|
||||
|
|
@ -358,15 +323,17 @@ defmodule Pleroma.User.BackupTest do
|
|||
%{http_method: :delete} -> {:ok, %{status_code: 204}}
|
||||
end)
|
||||
|
||||
assert {:ok, %Pleroma.Upload{}} = Backup.upload(backup, path)
|
||||
assert {:ok, _backup} = Backup.delete(backup)
|
||||
assert {:ok, backup} = Backup.run(backup)
|
||||
assert {:ok, %Backup{processed: true}} = Backup.upload(backup)
|
||||
assert {:ok, _backup} = Backup.delete_archive(backup)
|
||||
end
|
||||
|
||||
test "Local", %{path: path, backup: backup} do
|
||||
test "Local", %{backup: backup} do
|
||||
clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
|
||||
|
||||
assert {:ok, %Pleroma.Upload{}} = Backup.upload(backup, path)
|
||||
assert {:ok, _backup} = Backup.delete(backup)
|
||||
assert {:ok, backup} = Backup.run(backup)
|
||||
assert {:ok, %Backup{processed: true}} = Backup.upload(backup)
|
||||
assert {:ok, _backup} = Backup.delete_archive(backup)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -25,11 +25,12 @@ defmodule Pleroma.User.ImportTest do
|
|||
user3.nickname
|
||||
]
|
||||
|
||||
{:ok, job} = User.Import.follow_import(user1, identifiers)
|
||||
{:ok, jobs} = User.Import.follows_import(user1, identifiers)
|
||||
|
||||
for job <- jobs do
|
||||
assert {:ok, %User{}} = ObanHelpers.perform(job)
|
||||
end
|
||||
|
||||
assert {:ok, result} = ObanHelpers.perform(job)
|
||||
assert is_list(result)
|
||||
assert result == [refresh_record(user2), refresh_record(user3)]
|
||||
assert User.following?(user1, user2)
|
||||
assert User.following?(user1, user3)
|
||||
end
|
||||
|
|
@ -44,11 +45,12 @@ defmodule Pleroma.User.ImportTest do
|
|||
user3.nickname
|
||||
]
|
||||
|
||||
{:ok, job} = User.Import.blocks_import(user1, identifiers)
|
||||
{:ok, jobs} = User.Import.blocks_import(user1, identifiers)
|
||||
|
||||
for job <- jobs do
|
||||
assert {:ok, %User{}} = ObanHelpers.perform(job)
|
||||
end
|
||||
|
||||
assert {:ok, result} = ObanHelpers.perform(job)
|
||||
assert is_list(result)
|
||||
assert result == [user2, user3]
|
||||
assert User.blocks?(user1, user2)
|
||||
assert User.blocks?(user1, user3)
|
||||
end
|
||||
|
|
@ -63,11 +65,12 @@ defmodule Pleroma.User.ImportTest do
|
|||
user3.nickname
|
||||
]
|
||||
|
||||
{:ok, job} = User.Import.mutes_import(user1, identifiers)
|
||||
{:ok, jobs} = User.Import.mutes_import(user1, identifiers)
|
||||
|
||||
for job <- jobs do
|
||||
assert {:ok, %User{}} = ObanHelpers.perform(job)
|
||||
end
|
||||
|
||||
assert {:ok, result} = ObanHelpers.perform(job)
|
||||
assert is_list(result)
|
||||
assert result == [user2, user3]
|
||||
assert User.mutes?(user1, user2)
|
||||
assert User.mutes?(user1, user3)
|
||||
end
|
||||
|
|
|
|||
195
test/pleroma/user/search_test.exs
Normal file
195
test/pleroma/user/search_test.exs
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.User.SearchTest do
|
||||
use Pleroma.DataCase, async: false
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
alias Pleroma.Instances
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
||||
describe "search/2 mention suggestions" do
|
||||
test "prioritizes followed/follower users before others" do
|
||||
user = insert(:user)
|
||||
|
||||
related =
|
||||
insert(:user,
|
||||
local: false,
|
||||
nickname: "hj@real.example",
|
||||
ap_id: "https://real.example/users/hj",
|
||||
last_status_at: ~N[2020-01-01 00:00:00]
|
||||
)
|
||||
|
||||
other = insert(:user, nickname: "hj", last_status_at: ~N[2020-01-02 00:00:00])
|
||||
|
||||
{:ok, _related, _user} = User.follow(related, user)
|
||||
|
||||
results = User.search("hj", for_user: user) |> Enum.map(& &1.id)
|
||||
|
||||
assert results == [related.id, other.id]
|
||||
end
|
||||
|
||||
test "orders followed/follower users by most recent activity" do
|
||||
user = insert(:user)
|
||||
|
||||
older =
|
||||
insert(:user,
|
||||
local: false,
|
||||
nickname: "ali@remote.example",
|
||||
ap_id: "https://remote.example/users/ali",
|
||||
last_status_at: ~N[2020-01-01 00:00:00]
|
||||
)
|
||||
|
||||
newer =
|
||||
insert(:user,
|
||||
local: false,
|
||||
nickname: "alia@remote.example",
|
||||
ap_id: "https://remote.example/users/alia",
|
||||
last_status_at: ~N[2020-01-02 00:00:00]
|
||||
)
|
||||
|
||||
{:ok, _user, _older} = User.follow(user, older)
|
||||
{:ok, _user, _newer} = User.follow(user, newer)
|
||||
|
||||
assert [newer.id, older.id] ==
|
||||
User.search("ali", for_user: user)
|
||||
|> Enum.map(& &1.id)
|
||||
end
|
||||
|
||||
test "groups followed/follower users first and sorts them by recency" do
|
||||
user = insert(:user)
|
||||
|
||||
following_newest =
|
||||
insert(:user,
|
||||
local: false,
|
||||
nickname: "mentiontesta@related.example",
|
||||
ap_id: "https://related.example/users/mentiontesta",
|
||||
last_status_at: ~N[2020-01-03 00:00:00]
|
||||
)
|
||||
|
||||
follower_middle =
|
||||
insert(:user,
|
||||
local: false,
|
||||
nickname: "mentiontestb@related.example",
|
||||
ap_id: "https://related.example/users/mentiontestb",
|
||||
last_status_at: ~N[2020-01-02 00:00:00]
|
||||
)
|
||||
|
||||
mutual_oldest =
|
||||
insert(:user,
|
||||
local: false,
|
||||
nickname: "mentiontestc@related.example",
|
||||
ap_id: "https://related.example/users/mentiontestc",
|
||||
last_status_at: ~N[2020-01-01 00:00:00]
|
||||
)
|
||||
|
||||
unrelated_newer =
|
||||
insert(:user,
|
||||
local: false,
|
||||
nickname: "mentiontestd@unrelated.example",
|
||||
ap_id: "https://unrelated.example/users/mentiontestd",
|
||||
last_status_at: ~N[2020-01-04 00:00:00]
|
||||
)
|
||||
|
||||
{:ok, _user, _following_newest} = User.follow(user, following_newest)
|
||||
{:ok, _follower_middle, _user} = User.follow(follower_middle, user)
|
||||
|
||||
{:ok, _user, _mutual_oldest} = User.follow(user, mutual_oldest)
|
||||
{:ok, _mutual_oldest, _user} = User.follow(mutual_oldest, user)
|
||||
|
||||
results = User.search("mentiontest", for_user: user)
|
||||
|
||||
assert Enum.map(results, & &1.id) ==
|
||||
[following_newest.id, follower_middle.id, mutual_oldest.id, unrelated_newer.id]
|
||||
end
|
||||
|
||||
test "uses last_active_at when last_status_at is missing" do
|
||||
user = insert(:user)
|
||||
|
||||
older =
|
||||
insert(:user,
|
||||
local: false,
|
||||
nickname: "activefallbacka@remote.example",
|
||||
ap_id: "https://remote.example/users/activefallbacka",
|
||||
last_status_at: nil,
|
||||
last_active_at: ~N[2020-01-01 00:00:00]
|
||||
)
|
||||
|
||||
newer =
|
||||
insert(:user,
|
||||
local: false,
|
||||
nickname: "activefallbackb@remote.example",
|
||||
ap_id: "https://remote.example/users/activefallbackb",
|
||||
last_status_at: nil,
|
||||
last_active_at: ~N[2020-01-02 00:00:00]
|
||||
)
|
||||
|
||||
{:ok, _user, _older} = User.follow(user, older)
|
||||
{:ok, _user, _newer} = User.follow(user, newer)
|
||||
|
||||
assert [newer.id, older.id] ==
|
||||
User.search("activefallback", for_user: user)
|
||||
|> Enum.map(& &1.id)
|
||||
end
|
||||
|
||||
test "does not return deactivated users even if related" do
|
||||
user = insert(:user)
|
||||
|
||||
active =
|
||||
insert(:user,
|
||||
local: false,
|
||||
nickname: "deactivatedtesta@remote.example",
|
||||
ap_id: "https://remote.example/users/deactivatedtesta",
|
||||
last_status_at: ~N[2020-01-02 00:00:00]
|
||||
)
|
||||
|
||||
deactivated =
|
||||
insert(:user,
|
||||
local: false,
|
||||
nickname: "deactivatedtestb@remote.example",
|
||||
ap_id: "https://remote.example/users/deactivatedtestb",
|
||||
last_status_at: ~N[2020-01-03 00:00:00]
|
||||
)
|
||||
|
||||
{:ok, _user, _active} = User.follow(user, active)
|
||||
{:ok, _user, _deactivated} = User.follow(user, deactivated)
|
||||
Repo.update!(Ecto.Changeset.change(deactivated, is_active: false))
|
||||
|
||||
results = User.search("deactivatedtest", for_user: user) |> Enum.map(& &1.id)
|
||||
|
||||
assert results == [active.id]
|
||||
end
|
||||
|
||||
test "does not return users from unreachable instances" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, _instance} = Instances.set_unreachable("dead.example")
|
||||
|
||||
dead =
|
||||
insert(:user,
|
||||
local: false,
|
||||
nickname: "ali@dead.example",
|
||||
ap_id: "https://dead.example/users/ali",
|
||||
last_status_at: ~N[2020-01-02 00:00:00]
|
||||
)
|
||||
|
||||
alive =
|
||||
insert(:user,
|
||||
local: false,
|
||||
nickname: "ali@alive.example",
|
||||
ap_id: "https://alive.example/users/ali",
|
||||
last_status_at: ~N[2020-01-02 00:00:00]
|
||||
)
|
||||
|
||||
{:ok, _user, _alive} = User.follow(user, alive)
|
||||
{:ok, _user, _dead} = User.follow(user, dead)
|
||||
|
||||
results = User.search("ali", for_user: user) |> Enum.map(& &1.id)
|
||||
|
||||
assert results == [alive.id]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,11 +3,12 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.UserRelationshipTest do
|
||||
alias Pleroma.DateTimeMock
|
||||
alias Pleroma.UserRelationship
|
||||
|
||||
use Pleroma.DataCase, async: false
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
import Mock
|
||||
import Mox
|
||||
import Pleroma.Factory
|
||||
|
||||
describe "*_exists?/2" do
|
||||
|
|
@ -52,6 +53,9 @@ defmodule Pleroma.UserRelationshipTest do
|
|||
end
|
||||
|
||||
test "creates user relationship record if it doesn't exist", %{users: [user1, user2]} do
|
||||
DateTimeMock
|
||||
|> stub_with(Pleroma.DateTime.Impl)
|
||||
|
||||
for relationship_type <- [
|
||||
:block,
|
||||
:mute,
|
||||
|
|
@ -80,13 +84,15 @@ defmodule Pleroma.UserRelationshipTest do
|
|||
end
|
||||
|
||||
test "if record already exists, returns it", %{users: [user1, user2]} do
|
||||
user_block =
|
||||
with_mock NaiveDateTime, [:passthrough], utc_now: fn -> ~N[2017-03-17 17:09:58] end do
|
||||
{:ok, %{inserted_at: ~N[2017-03-17 17:09:58]}} =
|
||||
UserRelationship.create_block(user1, user2)
|
||||
end
|
||||
fixed_datetime = ~N[2017-03-17 17:09:58]
|
||||
|
||||
assert user_block == UserRelationship.create_block(user1, user2)
|
||||
Pleroma.DateTimeMock
|
||||
|> expect(:utc_now, 2, fn -> fixed_datetime end)
|
||||
|
||||
{:ok, %{inserted_at: ^fixed_datetime}} = UserRelationship.create_block(user1, user2)
|
||||
|
||||
# Test the idempotency without caring about the exact time
|
||||
assert {:ok, _} = UserRelationship.create_block(user1, user2)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -366,5 +366,13 @@ defmodule Pleroma.UserSearchTest do
|
|||
|
||||
assert user == result |> Map.put(:search_rank, nil) |> Map.put(:search_type, nil)
|
||||
end
|
||||
|
||||
test "find users accepting chat messages only" do
|
||||
user1 = insert(:user, nickname: "user1", accepts_chat_messages: true)
|
||||
insert(:user, nickname: "user2", accepts_chat_messages: false)
|
||||
|
||||
[found_user1] = User.search("user", capabilities: ["accepts_chat_messages"])
|
||||
assert found_user1.id == user1.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ defmodule Pleroma.UserTest do
|
|||
import Swoosh.TestAssertions
|
||||
|
||||
setup do
|
||||
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
|
||||
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
|
||||
:ok
|
||||
end
|
||||
|
||||
|
|
@ -182,8 +182,8 @@ defmodule Pleroma.UserTest do
|
|||
locked = insert(:user, is_locked: true)
|
||||
follower = insert(:user)
|
||||
|
||||
CommonAPI.follow(follower, unlocked)
|
||||
CommonAPI.follow(follower, locked)
|
||||
CommonAPI.follow(unlocked, follower)
|
||||
CommonAPI.follow(locked, follower)
|
||||
|
||||
assert [] = User.get_follow_requests(unlocked)
|
||||
assert [activity] = User.get_follow_requests(locked)
|
||||
|
|
@ -196,9 +196,9 @@ defmodule Pleroma.UserTest do
|
|||
pending_follower = insert(:user)
|
||||
accepted_follower = insert(:user)
|
||||
|
||||
CommonAPI.follow(pending_follower, locked)
|
||||
CommonAPI.follow(pending_follower, locked)
|
||||
CommonAPI.follow(accepted_follower, locked)
|
||||
CommonAPI.follow(locked, pending_follower)
|
||||
CommonAPI.follow(locked, pending_follower)
|
||||
CommonAPI.follow(locked, accepted_follower)
|
||||
|
||||
Pleroma.FollowingRelationship.update(accepted_follower, locked, :follow_accept)
|
||||
|
||||
|
|
@ -209,7 +209,7 @@ defmodule Pleroma.UserTest do
|
|||
locked = insert(:user, is_locked: true)
|
||||
pending_follower = insert(:user, %{is_active: false})
|
||||
|
||||
CommonAPI.follow(pending_follower, locked)
|
||||
CommonAPI.follow(locked, pending_follower)
|
||||
|
||||
refute pending_follower.is_active
|
||||
assert [] = User.get_follow_requests(locked)
|
||||
|
|
@ -219,7 +219,7 @@ defmodule Pleroma.UserTest do
|
|||
followed = insert(:user, is_locked: true)
|
||||
follower = insert(:user)
|
||||
|
||||
CommonAPI.follow(follower, followed)
|
||||
CommonAPI.follow(followed, follower)
|
||||
assert [_activity] = User.get_follow_requests(followed)
|
||||
|
||||
{:ok, _user_relationship} = User.block(followed, follower)
|
||||
|
|
@ -876,17 +876,17 @@ defmodule Pleroma.UserTest do
|
|||
describe "get_or_fetch/1 remote users with tld, while BE is running on a subdomain" do
|
||||
setup do: clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true)
|
||||
|
||||
test "for mastodon" do
|
||||
ap_id = "a@mastodon.example"
|
||||
{:ok, fetched_user} = User.get_or_fetch(ap_id)
|
||||
test "fetches a mastodon split-domain nickname" do
|
||||
nickname = "a@mastodon.example"
|
||||
{:ok, fetched_user} = User.get_or_fetch(nickname)
|
||||
|
||||
assert fetched_user.ap_id == "https://sub.mastodon.example/users/a"
|
||||
assert fetched_user.nickname == "a@mastodon.example"
|
||||
end
|
||||
|
||||
test "for pleroma" do
|
||||
ap_id = "a@pleroma.example"
|
||||
{:ok, fetched_user} = User.get_or_fetch(ap_id)
|
||||
test "fetches a pleroma split-domain nickname" do
|
||||
nickname = "a@pleroma.example"
|
||||
{:ok, fetched_user} = User.get_or_fetch(nickname)
|
||||
|
||||
assert fetched_user.ap_id == "https://sub.pleroma.example/users/a"
|
||||
assert fetched_user.nickname == "a@pleroma.example"
|
||||
|
|
@ -926,7 +926,6 @@ defmodule Pleroma.UserTest do
|
|||
assert user == fetched_user
|
||||
end
|
||||
|
||||
@tag capture_log: true
|
||||
test "returns nil if no user could be fetched" do
|
||||
{:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistent@social.heldscal.la")
|
||||
assert fetched_user == "not found nonexistent@social.heldscal.la"
|
||||
|
|
@ -937,6 +936,89 @@ defmodule Pleroma.UserTest do
|
|||
assert fetched_user == "not found nonexistent"
|
||||
end
|
||||
|
||||
test "does not rename an existing remote actor from rogue WebFinger data" do
|
||||
clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true)
|
||||
|
||||
actor_id = "https://legit-actor.example/users/alice"
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{url: "https://evil-webfinger.example/.well-known/host-meta"} ->
|
||||
{:ok, %Tesla.Env{status: 404}}
|
||||
|
||||
%{
|
||||
url:
|
||||
"https://evil-webfinger.example/.well-known/webfinger?resource=acct:claimed@evil-webfinger.example"
|
||||
} ->
|
||||
Tesla.Mock.json(%{
|
||||
"subject" => "acct:claimed@evil-webfinger.example",
|
||||
"links" => [
|
||||
%{
|
||||
"rel" => "self",
|
||||
"type" => "application/activity+json",
|
||||
"href" => actor_id
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
%{url: ^actor_id} ->
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
headers: [{"content-type", "application/activity+json"}],
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
"id" => actor_id,
|
||||
"type" => "Person",
|
||||
"preferredUsername" => "alice",
|
||||
"name" => "Alice",
|
||||
"summary" => "",
|
||||
"inbox" => "https://legit-actor.example/users/alice/inbox",
|
||||
"outbox" => "https://legit-actor.example/users/alice/outbox",
|
||||
"followers" => "https://legit-actor.example/users/alice/followers",
|
||||
"following" => "https://legit-actor.example/users/alice/following"
|
||||
})
|
||||
}}
|
||||
|
||||
%{url: "https://legit-actor.example/.well-known/host-meta"} ->
|
||||
{:ok, %Tesla.Env{status: 404}}
|
||||
|
||||
%{
|
||||
url:
|
||||
"https://legit-actor.example/.well-known/webfinger?resource=acct:alice@legit-actor.example"
|
||||
} ->
|
||||
Tesla.Mock.json(%{
|
||||
"subject" => "acct:alice@legit-actor.example",
|
||||
"links" => [
|
||||
%{
|
||||
"rel" => "self",
|
||||
"type" => "application/activity+json",
|
||||
"href" => actor_id
|
||||
}
|
||||
]
|
||||
})
|
||||
end)
|
||||
|
||||
assert {:error, {:webfinger_actor_mismatch, "claimed@evil-webfinger.example", ^actor_id}} =
|
||||
ActivityPub.make_user_from_nickname("claimed@evil-webfinger.example")
|
||||
|
||||
refute User.get_by_ap_id(actor_id)
|
||||
refute User.get_by_nickname("claimed@evil-webfinger.example")
|
||||
|
||||
orig_user =
|
||||
insert(:user,
|
||||
local: false,
|
||||
nickname: "alice@legit-actor.example",
|
||||
ap_id: actor_id
|
||||
)
|
||||
|
||||
assert {:error, {:webfinger_actor_mismatch, "claimed@evil-webfinger.example", ^actor_id}} =
|
||||
ActivityPub.make_user_from_nickname("claimed@evil-webfinger.example")
|
||||
|
||||
assert {:error, _} = User.get_or_fetch_by_nickname("claimed@evil-webfinger.example")
|
||||
assert User.get_by_id(orig_user.id).nickname == "alice@legit-actor.example"
|
||||
refute User.get_by_nickname("claimed@evil-webfinger.example")
|
||||
end
|
||||
|
||||
test "updates an existing user, if stale" do
|
||||
a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800)
|
||||
|
||||
|
|
@ -953,9 +1035,16 @@ defmodule Pleroma.UserTest do
|
|||
|
||||
{:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin")
|
||||
|
||||
assert user.inbox
|
||||
# Oban job was generated to refresh the stale user
|
||||
assert_enqueued(worker: "Pleroma.Workers.UserRefreshWorker", args: %{"ap_id" => user.ap_id})
|
||||
|
||||
refute user.last_refreshed_at == orig_user.last_refreshed_at
|
||||
# Run job to refresh the user; just capture its output instead of fetching it again
|
||||
assert {:ok, updated_user} =
|
||||
perform_job(Pleroma.Workers.UserRefreshWorker, %{"ap_id" => user.ap_id})
|
||||
|
||||
assert updated_user.inbox
|
||||
|
||||
refute updated_user.last_refreshed_at == orig_user.last_refreshed_at
|
||||
end
|
||||
|
||||
test "if nicknames clash, the old user gets a prefix with the old id to the nickname" do
|
||||
|
|
@ -983,7 +1072,6 @@ defmodule Pleroma.UserTest do
|
|||
assert orig_user.nickname == "#{orig_user.id}.admin@mastodon.example.org"
|
||||
end
|
||||
|
||||
@tag capture_log: true
|
||||
test "it returns the old user if stale, but unfetchable" do
|
||||
a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800)
|
||||
|
||||
|
|
@ -1070,6 +1158,21 @@ defmodule Pleroma.UserTest do
|
|||
|
||||
refute cs.valid?
|
||||
end
|
||||
|
||||
test "it truncates fields" do
|
||||
clear_config([:instance, :max_remote_account_fields], 2)
|
||||
|
||||
fields = [
|
||||
%{"name" => "One", "value" => "Uno"},
|
||||
%{"name" => "Two", "value" => "Dos"},
|
||||
%{"name" => "Three", "value" => "Tres"}
|
||||
]
|
||||
|
||||
cs = User.remote_user_changeset(@valid_remote |> Map.put(:fields, fields))
|
||||
|
||||
assert [%{"name" => "One", "value" => "Uno"}, %{"name" => "Two", "value" => "Dos"}] ==
|
||||
Ecto.Changeset.get_field(cs, :fields)
|
||||
end
|
||||
end
|
||||
|
||||
describe "followers and friends" do
|
||||
|
|
@ -1521,7 +1624,7 @@ defmodule Pleroma.UserTest do
|
|||
|
||||
assert [activity] == ActivityPub.fetch_public_activities(%{}) |> Repo.preload(:bookmark)
|
||||
|
||||
assert [%{activity | thread_muted?: CommonAPI.thread_muted?(user2, activity)}] ==
|
||||
assert [%{activity | thread_muted?: CommonAPI.thread_muted?(activity, user2)}] ==
|
||||
ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{
|
||||
user: user2
|
||||
})
|
||||
|
|
@ -1686,8 +1789,8 @@ defmodule Pleroma.UserTest do
|
|||
object_two = insert(:note, user: follower)
|
||||
activity_two = insert(:note_activity, user: follower, note: object_two)
|
||||
|
||||
{:ok, like} = CommonAPI.favorite(user, activity_two.id)
|
||||
{:ok, like_two} = CommonAPI.favorite(follower, activity.id)
|
||||
{:ok, like} = CommonAPI.favorite(activity_two.id, user)
|
||||
{:ok, like_two} = CommonAPI.favorite(activity.id, follower)
|
||||
{:ok, repeat} = CommonAPI.repeat(activity_two.id, user)
|
||||
|
||||
{:ok, job} = User.delete(user)
|
||||
|
|
@ -1861,6 +1964,11 @@ defmodule Pleroma.UserTest do
|
|||
end
|
||||
end
|
||||
|
||||
test "get_or_fetch_public_key_for_ap_id fetches a user that's not in the db" do
|
||||
assert {:ok, _key} =
|
||||
User.get_or_fetch_public_key_for_ap_id("http://mastodon.example.org/users/admin")
|
||||
end
|
||||
|
||||
test "get_public_key_for_ap_id returns correctly for user that's not in the db" do
|
||||
assert :error = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin")
|
||||
end
|
||||
|
|
@ -1880,14 +1988,26 @@ defmodule Pleroma.UserTest do
|
|||
end
|
||||
|
||||
describe "caching" do
|
||||
test "set_cache stores friends under the AP ID cache key" do
|
||||
user = insert(:user)
|
||||
|
||||
User.set_cache(user)
|
||||
|
||||
assert {:ok, []} = Cachex.get(:user_cache, "friends_ap_ids:#{user.ap_id}")
|
||||
assert {:ok, nil} = Cachex.get(:user_cache, "friends_ap_ids:#{user.nickname}")
|
||||
end
|
||||
|
||||
test "invalidate_cache works" do
|
||||
user = insert(:user)
|
||||
|
||||
User.set_cache(user)
|
||||
User.get_cached_by_id(user.id)
|
||||
User.invalidate_cache(user)
|
||||
|
||||
{:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
|
||||
{:ok, nil} = Cachex.get(:user_cache, "nickname:#{user.nickname}")
|
||||
{:ok, nil} = Cachex.get(:user_cache, "friends_ap_ids:#{user.ap_id}")
|
||||
{:ok, nil} = Cachex.get(:user_cache, "id:#{user.id}")
|
||||
end
|
||||
|
||||
test "User.delete() plugs any possible zombie objects" do
|
||||
|
|
@ -2385,8 +2505,8 @@ defmodule Pleroma.UserTest do
|
|||
other_user =
|
||||
insert(:user,
|
||||
local: false,
|
||||
follower_address: "http://localhost:4001/users/masto_closed/followers",
|
||||
following_address: "http://localhost:4001/users/masto_closed/following"
|
||||
follower_address: "https://remote.org/users/masto_closed/followers",
|
||||
following_address: "https://remote.org/users/masto_closed/following"
|
||||
)
|
||||
|
||||
assert other_user.following_count == 0
|
||||
|
|
@ -2406,8 +2526,8 @@ defmodule Pleroma.UserTest do
|
|||
other_user =
|
||||
insert(:user,
|
||||
local: false,
|
||||
follower_address: "http://localhost:4001/users/masto_closed/followers",
|
||||
following_address: "http://localhost:4001/users/masto_closed/following"
|
||||
follower_address: "https://remote.org/users/masto_closed/followers",
|
||||
following_address: "https://remote.org/users/masto_closed/following"
|
||||
)
|
||||
|
||||
assert other_user.following_count == 0
|
||||
|
|
@ -2427,8 +2547,8 @@ defmodule Pleroma.UserTest do
|
|||
other_user =
|
||||
insert(:user,
|
||||
local: false,
|
||||
follower_address: "http://localhost:4001/users/masto_closed/followers",
|
||||
following_address: "http://localhost:4001/users/masto_closed/following"
|
||||
follower_address: "https://remote.org/users/masto_closed/followers",
|
||||
following_address: "https://remote.org/users/masto_closed/following"
|
||||
)
|
||||
|
||||
assert other_user.following_count == 0
|
||||
|
|
@ -2649,8 +2769,12 @@ defmodule Pleroma.UserTest do
|
|||
|
||||
assert {:ok, user} = User.update_last_active_at(user)
|
||||
|
||||
assert user.last_active_at >= test_started_at
|
||||
assert user.last_active_at <= NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
|
||||
assert NaiveDateTime.compare(user.last_active_at, test_started_at) in [:gt, :eq]
|
||||
|
||||
assert NaiveDateTime.compare(
|
||||
user.last_active_at,
|
||||
NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
|
||||
) in [:lt, :eq]
|
||||
|
||||
last_active_at =
|
||||
NaiveDateTime.utc_now()
|
||||
|
|
@ -2662,10 +2786,15 @@ defmodule Pleroma.UserTest do
|
|||
|> cast(%{last_active_at: last_active_at}, [:last_active_at])
|
||||
|> User.update_and_set_cache()
|
||||
|
||||
assert user.last_active_at == last_active_at
|
||||
assert NaiveDateTime.compare(user.last_active_at, last_active_at) == :eq
|
||||
|
||||
assert {:ok, user} = User.update_last_active_at(user)
|
||||
assert user.last_active_at >= test_started_at
|
||||
assert user.last_active_at <= NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
|
||||
assert NaiveDateTime.compare(user.last_active_at, test_started_at) in [:gt, :eq]
|
||||
|
||||
assert NaiveDateTime.compare(
|
||||
user.last_active_at,
|
||||
NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
|
||||
) in [:lt, :eq]
|
||||
end
|
||||
|
||||
test "active_user_count/1" do
|
||||
|
|
@ -2763,6 +2892,15 @@ defmodule Pleroma.UserTest do
|
|||
assert user_updated.also_known_as |> length() == 1
|
||||
assert user2.ap_id in user_updated.also_known_as
|
||||
end
|
||||
|
||||
test "should tolerate non-http(s) aliases" do
|
||||
user =
|
||||
insert(:user, %{
|
||||
also_known_as: ["at://did:plc:xgvzy7ni6ig6ievcbls5jaxe"]
|
||||
})
|
||||
|
||||
assert "at://did:plc:xgvzy7ni6ig6ievcbls5jaxe" in user.also_known_as
|
||||
end
|
||||
end
|
||||
|
||||
describe "alias_users/1" do
|
||||
|
|
@ -2899,4 +3037,74 @@ defmodule Pleroma.UserTest do
|
|||
|
||||
assert [%{"verified_at" => ^verified_at}] = user.fields
|
||||
end
|
||||
|
||||
describe "follow_hashtag/2" do
|
||||
test "should follow a hashtag" do
|
||||
user = insert(:user)
|
||||
hashtag = insert(:hashtag)
|
||||
|
||||
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||
|
||||
user = User.get_cached_by_ap_id(user.ap_id)
|
||||
|
||||
assert user.followed_hashtags |> Enum.count() == 1
|
||||
assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
|
||||
end
|
||||
|
||||
test "should not follow a hashtag twice" do
|
||||
user = insert(:user)
|
||||
hashtag = insert(:hashtag)
|
||||
|
||||
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||
|
||||
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||
|
||||
user = User.get_cached_by_ap_id(user.ap_id)
|
||||
|
||||
assert user.followed_hashtags |> Enum.count() == 1
|
||||
assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
|
||||
end
|
||||
|
||||
test "can follow multiple hashtags" do
|
||||
user = insert(:user)
|
||||
hashtag = insert(:hashtag)
|
||||
other_hashtag = insert(:hashtag)
|
||||
|
||||
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||
assert {:ok, _} = user |> User.follow_hashtag(other_hashtag)
|
||||
|
||||
user = User.get_cached_by_ap_id(user.ap_id)
|
||||
|
||||
assert user.followed_hashtags |> Enum.count() == 2
|
||||
assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
|
||||
assert other_hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "unfollow_hashtag/2" do
|
||||
test "should unfollow a hashtag" do
|
||||
user = insert(:user)
|
||||
hashtag = insert(:hashtag)
|
||||
|
||||
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||
assert {:ok, _} = user |> User.unfollow_hashtag(hashtag)
|
||||
|
||||
user = User.get_cached_by_ap_id(user.ap_id)
|
||||
|
||||
assert user.followed_hashtags |> Enum.count() == 0
|
||||
end
|
||||
|
||||
test "should not error when trying to unfollow a hashtag twice" do
|
||||
user = insert(:user)
|
||||
hashtag = insert(:hashtag)
|
||||
|
||||
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||
assert {:ok, _} = user |> User.unfollow_hashtag(hashtag)
|
||||
assert {:ok, _} = user |> User.unfollow_hashtag(hashtag)
|
||||
|
||||
user = User.get_cached_by_ap_id(user.ap_id)
|
||||
|
||||
assert user.followed_hashtags |> Enum.count() == 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -232,12 +232,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
|
||||
assert user.avatar == %{
|
||||
"type" => "Image",
|
||||
"url" => [%{"href" => "https://jk.nipponalba.scot/images/profile.jpg"}]
|
||||
"url" => [%{"href" => "https://jk.nipponalba.scot/images/profile.jpg"}],
|
||||
"name" => "profile picture"
|
||||
}
|
||||
|
||||
assert user.banner == %{
|
||||
"type" => "Image",
|
||||
"url" => [%{"href" => "https://jk.nipponalba.scot/images/profile.jpg"}]
|
||||
"url" => [%{"href" => "https://jk.nipponalba.scot/images/profile.jpg"}],
|
||||
"name" => "profile picture"
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -291,9 +293,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
body: featured_data,
|
||||
headers: [{"content-type", "application/activity+json"}]
|
||||
}
|
||||
end)
|
||||
|
||||
Tesla.Mock.mock_global(fn
|
||||
%{
|
||||
method: :get,
|
||||
url: ^object_url
|
||||
|
|
@ -306,7 +306,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
end)
|
||||
|
||||
{:ok, user} = ActivityPub.make_user_from_ap_id(ap_id)
|
||||
Process.sleep(50)
|
||||
|
||||
assert_enqueued(
|
||||
worker: Pleroma.Workers.RemoteFetcherWorker,
|
||||
args: %{
|
||||
"op" => "fetch_remote",
|
||||
"id" => object_url,
|
||||
"depth" => 1
|
||||
}
|
||||
)
|
||||
|
||||
# wait for oban
|
||||
Pleroma.Tests.ObanHelpers.perform_all()
|
||||
|
||||
assert user.featured_address == featured_url
|
||||
assert Map.has_key?(user.pinned_objects, object_url)
|
||||
|
|
@ -368,9 +379,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
body: featured_data,
|
||||
headers: [{"content-type", "application/activity+json"}]
|
||||
}
|
||||
end)
|
||||
|
||||
Tesla.Mock.mock_global(fn
|
||||
%{
|
||||
method: :get,
|
||||
url: ^object_url
|
||||
|
|
@ -383,7 +392,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
end)
|
||||
|
||||
{:ok, user} = ActivityPub.make_user_from_ap_id(ap_id)
|
||||
Process.sleep(50)
|
||||
|
||||
assert_enqueued(
|
||||
worker: Pleroma.Workers.RemoteFetcherWorker,
|
||||
args: %{
|
||||
"op" => "fetch_remote",
|
||||
"id" => object_url,
|
||||
"depth" => 1
|
||||
}
|
||||
)
|
||||
|
||||
# wait for oban
|
||||
Pleroma.Tests.ObanHelpers.perform_all()
|
||||
|
||||
assert user.featured_address == featured_url
|
||||
assert Map.has_key?(user.pinned_objects, object_url)
|
||||
|
|
@ -395,6 +415,105 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
assert %{data: %{"id" => ^object_url}} = Object.get_by_ap_id(object_url)
|
||||
end
|
||||
|
||||
test "fetches user featured collection from the first collection page" do
|
||||
ap_id = "https://example.com/users/lain"
|
||||
featured_url = "https://example.com/users/lain/collections/featured"
|
||||
first_page_url = "#{featured_url}?page=true"
|
||||
|
||||
user_data =
|
||||
"test/fixtures/users_mock/user.json"
|
||||
|> File.read!()
|
||||
|> String.replace("{{nickname}}", "lain")
|
||||
|> Jason.decode!()
|
||||
|> Map.put("featured", featured_url)
|
||||
|> Jason.encode!()
|
||||
|
||||
object_id = Ecto.UUID.generate()
|
||||
object_url = "https://example.com/objects/#{object_id}"
|
||||
|
||||
featured_data =
|
||||
Jason.encode!(%{
|
||||
"id" => featured_url,
|
||||
"type" => "OrderedCollection",
|
||||
"first" => first_page_url
|
||||
})
|
||||
|
||||
first_page_data =
|
||||
Jason.encode!(%{
|
||||
"id" => first_page_url,
|
||||
"type" => "OrderedCollectionPage",
|
||||
"partOf" => featured_url,
|
||||
"orderedItems" => [object_url]
|
||||
})
|
||||
|
||||
object_data =
|
||||
"test/fixtures/statuses/note.json"
|
||||
|> File.read!()
|
||||
|> String.replace("{{object_id}}", object_id)
|
||||
|> String.replace("{{nickname}}", "lain")
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{
|
||||
method: :get,
|
||||
url: ^ap_id
|
||||
} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: user_data,
|
||||
headers: [{"content-type", "application/activity+json"}]
|
||||
}
|
||||
|
||||
%{
|
||||
method: :get,
|
||||
url: ^featured_url
|
||||
} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: featured_data,
|
||||
headers: [{"content-type", "application/activity+json"}]
|
||||
}
|
||||
|
||||
%{
|
||||
method: :get,
|
||||
url: ^first_page_url
|
||||
} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: first_page_data,
|
||||
headers: [{"content-type", "application/activity+json"}]
|
||||
}
|
||||
|
||||
%{
|
||||
method: :get,
|
||||
url: ^object_url
|
||||
} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: object_data,
|
||||
headers: [{"content-type", "application/activity+json"}]
|
||||
}
|
||||
end)
|
||||
|
||||
refute capture_log(fn ->
|
||||
{:ok, user} = ActivityPub.make_user_from_ap_id(ap_id)
|
||||
|
||||
assert_enqueued(
|
||||
worker: Pleroma.Workers.RemoteFetcherWorker,
|
||||
args: %{
|
||||
"op" => "fetch_remote",
|
||||
"id" => object_url,
|
||||
"depth" => 1
|
||||
}
|
||||
)
|
||||
|
||||
Pleroma.Tests.ObanHelpers.perform_all()
|
||||
|
||||
assert user.featured_address == featured_url
|
||||
assert Map.has_key?(user.pinned_objects, object_url)
|
||||
assert %{data: %{"id" => ^object_url}} = Object.get_by_ap_id(object_url)
|
||||
end) =~ "Could not parse featured collection"
|
||||
end
|
||||
|
||||
test "fetches user birthday information from misskey" do
|
||||
user_id = "https://misskey.io/@mkljczk"
|
||||
|
||||
|
|
@ -414,6 +533,98 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
|
||||
assert user.birthday == ~D[2001-02-12]
|
||||
end
|
||||
|
||||
test "fetches avatar description" do
|
||||
user_id = "https://example.com/users/marcin"
|
||||
|
||||
user_data =
|
||||
"test/fixtures/users_mock/user.json"
|
||||
|> File.read!()
|
||||
|> String.replace("{{nickname}}", "marcin")
|
||||
|> Jason.decode!()
|
||||
|> Map.delete("featured")
|
||||
|> Map.update("icon", %{}, fn image -> Map.put(image, "name", "image description") end)
|
||||
|> Jason.encode!()
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{
|
||||
method: :get,
|
||||
url: ^user_id
|
||||
} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: user_data,
|
||||
headers: [{"content-type", "application/activity+json"}]
|
||||
}
|
||||
end)
|
||||
|
||||
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
|
||||
|
||||
assert user.avatar["name"] == "image description"
|
||||
end
|
||||
end
|
||||
|
||||
test "works with avatar/banner href as list" do
|
||||
user_id = "https://queef.in/cute_cat"
|
||||
|
||||
user_data =
|
||||
"test/fixtures/users_mock/href_as_array.json"
|
||||
|> File.read!()
|
||||
|> Jason.decode!()
|
||||
|> Map.delete("featured")
|
||||
|> Jason.encode!()
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{
|
||||
method: :get,
|
||||
url: ^user_id
|
||||
} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: user_data,
|
||||
headers: [{"content-type", "application/activity+json"}]
|
||||
}
|
||||
end)
|
||||
|
||||
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
|
||||
|
||||
assert length(user.avatar["url"]) == 1
|
||||
assert length(user.banner["url"]) == 1
|
||||
|
||||
assert user.avatar["url"] |> List.first() |> Map.fetch!("href") ==
|
||||
"https://queef.in/storage/profile.webp"
|
||||
|
||||
assert user.banner["url"] |> List.first() |> Map.fetch!("href") ==
|
||||
"https://queef.in/storage/banner.gif"
|
||||
end
|
||||
|
||||
test "works with alsoKnownAs as string" do
|
||||
user_id = "https://hub.netzgemeinde.eu/channel/jupiter_rowland"
|
||||
|
||||
user_data =
|
||||
"test/fixtures/users_mock/hubzilla-actor-alsoknownas-string.json"
|
||||
|> File.read!()
|
||||
|
||||
user_data_decoded =
|
||||
user_data
|
||||
|> Jason.decode!()
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{
|
||||
method: :get,
|
||||
url: ^user_id
|
||||
} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: user_data,
|
||||
headers: [{"content-type", "application/activity+json"}]
|
||||
}
|
||||
end)
|
||||
|
||||
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
|
||||
|
||||
assert is_list(user.also_known_as)
|
||||
assert user.also_known_as == [user_data_decoded["alsoKnownAs"]]
|
||||
end
|
||||
|
||||
test "it fetches the appropriate tag-restricted posts" do
|
||||
|
|
@ -777,12 +988,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
assert object.data["repliesCount"] == 2
|
||||
end
|
||||
|
||||
test "increates quotes count", %{user: user} do
|
||||
test "increases quotes count", %{user: user} do
|
||||
user2 = insert(:user)
|
||||
|
||||
{:ok, activity} = CommonAPI.post(user, %{status: "1", visibility: "public"})
|
||||
ap_id = activity.data["id"]
|
||||
quote_data = %{status: "1", quote_id: activity.id}
|
||||
quote_data = %{status: "1", quoted_status_id: activity.id}
|
||||
|
||||
# public
|
||||
{:ok, _} = CommonAPI.post(user2, Map.put(quote_data, :visibility, "public"))
|
||||
|
|
@ -818,6 +1029,33 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "fetch activities for followed hashtags" do
|
||||
test "it should return public activities that reference a given hashtag" do
|
||||
hashtag = insert(:hashtag, name: "tenshi")
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, normally_visible} =
|
||||
CommonAPI.post(other_user, %{status: "hello :)", visibility: "public"})
|
||||
|
||||
{:ok, public} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "public"})
|
||||
{:ok, _unrelated} = CommonAPI.post(user, %{status: "dai #tensh", visibility: "public"})
|
||||
{:ok, unlisted} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "unlisted"})
|
||||
{:ok, _private} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "private"})
|
||||
|
||||
activities =
|
||||
ActivityPub.fetch_activities([other_user.follower_address], %{
|
||||
followed_hashtags: [hashtag.id]
|
||||
})
|
||||
|
||||
assert length(activities) == 3
|
||||
normal_id = normally_visible.id
|
||||
public_id = public.id
|
||||
unlisted_id = unlisted.id
|
||||
assert [%{id: ^normal_id}, %{id: ^public_id}, %{id: ^unlisted_id}] = activities
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch activities in context" do
|
||||
test "retrieves activities that have a given context" do
|
||||
{:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
|
||||
|
|
@ -1020,7 +1258,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
refute activity in activities
|
||||
|
||||
followed_user = insert(:user)
|
||||
CommonAPI.follow(user, followed_user)
|
||||
CommonAPI.follow(followed_user, user)
|
||||
{:ok, repeat_activity} = CommonAPI.repeat(activity.id, followed_user)
|
||||
|
||||
activities = ActivityPub.fetch_activities([], %{blocking_user: user, skip_preload: true})
|
||||
|
|
@ -1153,7 +1391,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
note_two = insert(:note, data: %{"context" => "suya.."})
|
||||
activity_two = insert(:note_activity, note: note_two)
|
||||
|
||||
{:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
|
||||
{:ok, _activity_two} = CommonAPI.add_mute(activity_two, user)
|
||||
|
||||
assert [_activity_one] = ActivityPub.fetch_activities([], %{muting_user: user})
|
||||
end
|
||||
|
|
@ -1164,7 +1402,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
note_two = insert(:note, data: %{"context" => "suya.."})
|
||||
activity_two = insert(:note_activity, note: note_two)
|
||||
|
||||
{:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
|
||||
{:ok, _activity_two} = CommonAPI.add_mute(activity_two, user)
|
||||
|
||||
assert [_activity_two, _activity_one] =
|
||||
ActivityPub.fetch_activities([], %{muting_user: user, with_muted: true})
|
||||
|
|
@ -1194,6 +1432,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
assert activity == expected_activity
|
||||
end
|
||||
|
||||
test "includes only reblogs on request" do
|
||||
user = insert(:user)
|
||||
{:ok, _} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user})
|
||||
{:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user})
|
||||
|
||||
[activity] = ActivityPub.fetch_user_activities(user, nil, %{only_reblogs: true})
|
||||
|
||||
assert activity == expected_activity
|
||||
end
|
||||
|
||||
describe "irreversible filters" do
|
||||
setup do
|
||||
user = insert(:user)
|
||||
|
|
@ -1340,7 +1588,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
activity = insert(:note_activity)
|
||||
user = insert(:user)
|
||||
booster = insert(:user)
|
||||
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster)
|
||||
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(booster, user)
|
||||
|
||||
{:ok, activity} = CommonAPI.repeat(activity.id, booster)
|
||||
|
||||
|
|
@ -1353,8 +1601,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
activity = insert(:note_activity)
|
||||
user = insert(:user)
|
||||
booster = insert(:user)
|
||||
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster)
|
||||
{:ok, _reblog_mute} = CommonAPI.show_reblogs(user, booster)
|
||||
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(booster, user)
|
||||
{:ok, _reblog_mute} = CommonAPI.show_reblogs(booster, user)
|
||||
|
||||
{:ok, activity} = CommonAPI.repeat(activity.id, booster)
|
||||
|
||||
|
|
@ -1375,8 +1623,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
%{test_file: test_file}
|
||||
end
|
||||
|
||||
test "strips / from filename", %{test_file: file} do
|
||||
file = %Plug.Upload{file | filename: "../../../../../nested/bad.jpg"}
|
||||
test "strips / from filename", %{test_file: %Plug.Upload{} = file} do
|
||||
file = %{file | filename: "../../../../../nested/bad.jpg"}
|
||||
{:ok, %Object{} = object} = ActivityPub.upload(file)
|
||||
[%{"href" => href}] = object.data["url"]
|
||||
assert Regex.match?(~r"/bad.jpg$", href)
|
||||
|
|
@ -1434,7 +1682,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
follower = insert(:user)
|
||||
followed = insert(:user)
|
||||
|
||||
{:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed)
|
||||
{:ok, _, _, follow_activity} = CommonAPI.follow(followed, follower)
|
||||
|
||||
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
|
||||
assert {:error, :reverted} = ActivityPub.unfollow(follower, followed)
|
||||
|
|
@ -1451,7 +1699,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
follower = insert(:user)
|
||||
followed = insert(:user)
|
||||
|
||||
{:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed)
|
||||
{:ok, _, _, follow_activity} = CommonAPI.follow(followed, follower)
|
||||
{:ok, activity} = ActivityPub.unfollow(follower, followed)
|
||||
|
||||
assert activity.data["type"] == "Undo"
|
||||
|
|
@ -1468,7 +1716,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
follower = insert(:user)
|
||||
followed = insert(:user, %{is_locked: true})
|
||||
|
||||
{:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed)
|
||||
{:ok, _, _, follow_activity} = CommonAPI.follow(followed, follower)
|
||||
{:ok, activity} = ActivityPub.unfollow(follower, followed)
|
||||
|
||||
assert activity.data["type"] == "Undo"
|
||||
|
|
@ -1605,32 +1853,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
} = activity
|
||||
end
|
||||
|
||||
test_with_mock "strips status data from Flag, before federating it",
|
||||
%{
|
||||
reporter: reporter,
|
||||
context: context,
|
||||
target_account: target_account,
|
||||
reported_activity: reported_activity,
|
||||
object_ap_id: object_ap_id,
|
||||
content: content
|
||||
},
|
||||
Utils,
|
||||
[:passthrough],
|
||||
[] do
|
||||
{:ok, activity} =
|
||||
ActivityPub.flag(%{
|
||||
actor: reporter,
|
||||
context: context,
|
||||
account: target_account,
|
||||
statuses: [reported_activity],
|
||||
content: content
|
||||
})
|
||||
|
||||
new_data = put_in(activity.data, ["object"], [target_account.ap_id, object_ap_id])
|
||||
|
||||
assert_called(Utils.maybe_federate(%{activity | data: new_data}))
|
||||
end
|
||||
|
||||
test_with_mock "reverts on error",
|
||||
%{
|
||||
reporter: reporter,
|
||||
|
|
@ -1660,13 +1882,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
test "fetch_activities/2 returns activities addressed to a list " do
|
||||
user = insert(:user)
|
||||
member = insert(:user)
|
||||
{:ok, list} = Pleroma.List.create("foo", user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "foo"}, user)
|
||||
{:ok, list} = Pleroma.List.follow(list, member)
|
||||
|
||||
{:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"})
|
||||
{:ok, %Activity{} = activity} =
|
||||
CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"})
|
||||
|
||||
activity = Repo.preload(activity, :bookmark)
|
||||
activity = %Activity{activity | thread_muted?: !!activity.thread_muted?}
|
||||
activity = %{activity | thread_muted?: !!activity.thread_muted?}
|
||||
|
||||
assert ActivityPub.fetch_activities([], %{user: user}) == [activity]
|
||||
end
|
||||
|
|
@ -1709,8 +1932,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
user =
|
||||
insert(:user,
|
||||
local: false,
|
||||
follower_address: "http://localhost:4001/users/fuser2/followers",
|
||||
following_address: "http://localhost:4001/users/fuser2/following"
|
||||
follower_address: "https://remote.org/users/fuser2/followers",
|
||||
following_address: "https://remote.org/users/fuser2/following"
|
||||
)
|
||||
|
||||
{:ok, info} = ActivityPub.fetch_follow_information_for_user(user)
|
||||
|
|
@ -1721,7 +1944,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
test "detects hidden followers" do
|
||||
mock(fn env ->
|
||||
case env.url do
|
||||
"http://localhost:4001/users/masto_closed/followers?page=1" ->
|
||||
"https://remote.org/users/masto_closed/followers?page=1" ->
|
||||
%Tesla.Env{status: 403, body: ""}
|
||||
|
||||
_ ->
|
||||
|
|
@ -1732,8 +1955,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
user =
|
||||
insert(:user,
|
||||
local: false,
|
||||
follower_address: "http://localhost:4001/users/masto_closed/followers",
|
||||
following_address: "http://localhost:4001/users/masto_closed/following"
|
||||
follower_address: "https://remote.org/users/masto_closed/followers",
|
||||
following_address: "https://remote.org/users/masto_closed/following"
|
||||
)
|
||||
|
||||
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
|
||||
|
|
@ -1744,7 +1967,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
test "detects hidden follows" do
|
||||
mock(fn env ->
|
||||
case env.url do
|
||||
"http://localhost:4001/users/masto_closed/following?page=1" ->
|
||||
"https://remote.org/users/masto_closed/following?page=1" ->
|
||||
%Tesla.Env{status: 403, body: ""}
|
||||
|
||||
_ ->
|
||||
|
|
@ -1755,8 +1978,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
user =
|
||||
insert(:user,
|
||||
local: false,
|
||||
follower_address: "http://localhost:4001/users/masto_closed/followers",
|
||||
following_address: "http://localhost:4001/users/masto_closed/following"
|
||||
follower_address: "https://remote.org/users/masto_closed/followers",
|
||||
following_address: "https://remote.org/users/masto_closed/following"
|
||||
)
|
||||
|
||||
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
|
||||
|
|
@ -1768,8 +1991,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
user =
|
||||
insert(:user,
|
||||
local: false,
|
||||
follower_address: "http://localhost:8080/followers/fuser3",
|
||||
following_address: "http://localhost:8080/following/fuser3"
|
||||
follower_address: "https://remote.org/followers/fuser3",
|
||||
following_address: "https://remote.org/following/fuser3"
|
||||
)
|
||||
|
||||
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
|
||||
|
|
@ -1782,28 +2005,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
test "doesn't crash when follower and following counters are hidden" do
|
||||
mock(fn env ->
|
||||
case env.url do
|
||||
"http://localhost:4001/users/masto_hidden_counters/following" ->
|
||||
"https://remote.org/users/masto_hidden_counters/following" ->
|
||||
json(
|
||||
%{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"id" => "http://localhost:4001/users/masto_hidden_counters/followers"
|
||||
"id" => "https://remote.org/users/masto_hidden_counters/followers"
|
||||
},
|
||||
headers: HttpRequestMock.activitypub_object_headers()
|
||||
)
|
||||
|
||||
"http://localhost:4001/users/masto_hidden_counters/following?page=1" ->
|
||||
"https://remote.org/users/masto_hidden_counters/following?page=1" ->
|
||||
%Tesla.Env{status: 403, body: ""}
|
||||
|
||||
"http://localhost:4001/users/masto_hidden_counters/followers" ->
|
||||
"https://remote.org/users/masto_hidden_counters/followers" ->
|
||||
json(
|
||||
%{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"id" => "http://localhost:4001/users/masto_hidden_counters/following"
|
||||
"id" => "https://remote.org/users/masto_hidden_counters/following"
|
||||
},
|
||||
headers: HttpRequestMock.activitypub_object_headers()
|
||||
)
|
||||
|
||||
"http://localhost:4001/users/masto_hidden_counters/followers?page=1" ->
|
||||
"https://remote.org/users/masto_hidden_counters/followers?page=1" ->
|
||||
%Tesla.Env{status: 403, body: ""}
|
||||
end
|
||||
end)
|
||||
|
|
@ -1811,8 +2034,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
user =
|
||||
insert(:user,
|
||||
local: false,
|
||||
follower_address: "http://localhost:4001/users/masto_hidden_counters/followers",
|
||||
following_address: "http://localhost:4001/users/masto_hidden_counters/following"
|
||||
follower_address: "https://remote.org/users/masto_hidden_counters/followers",
|
||||
following_address: "https://remote.org/users/masto_hidden_counters/following"
|
||||
)
|
||||
|
||||
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
|
||||
|
|
@ -1836,14 +2059,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
{:ok, a4} = CommonAPI.post(user2, %{status: "Agent Smith "})
|
||||
{:ok, a5} = CommonAPI.post(user1, %{status: "Red or Blue "})
|
||||
|
||||
{:ok, _} = CommonAPI.favorite(user, a4.id)
|
||||
{:ok, _} = CommonAPI.favorite(other_user, a3.id)
|
||||
{:ok, _} = CommonAPI.favorite(user, a3.id)
|
||||
{:ok, _} = CommonAPI.favorite(other_user, a5.id)
|
||||
{:ok, _} = CommonAPI.favorite(user, a5.id)
|
||||
{:ok, _} = CommonAPI.favorite(other_user, a4.id)
|
||||
{:ok, _} = CommonAPI.favorite(user, a1.id)
|
||||
{:ok, _} = CommonAPI.favorite(other_user, a1.id)
|
||||
{:ok, _} = CommonAPI.favorite(a4.id, user)
|
||||
{:ok, _} = CommonAPI.favorite(a3.id, other_user)
|
||||
{:ok, _} = CommonAPI.favorite(a3.id, user)
|
||||
{:ok, _} = CommonAPI.favorite(a5.id, other_user)
|
||||
{:ok, _} = CommonAPI.favorite(a5.id, user)
|
||||
{:ok, _} = CommonAPI.favorite(a4.id, other_user)
|
||||
{:ok, _} = CommonAPI.favorite(a1.id, user)
|
||||
{:ok, _} = CommonAPI.favorite(a1.id, other_user)
|
||||
result = ActivityPub.fetch_favourites(user)
|
||||
|
||||
assert Enum.map(result, & &1.id) == [a1.id, a5.id, a3.id, a4.id]
|
||||
|
|
@ -1866,7 +2089,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
assert User.following?(follower, old_user)
|
||||
assert User.following?(follower_move_opted_out, old_user)
|
||||
|
||||
assert {:ok, activity} = ActivityPub.move(old_user, new_user)
|
||||
assert {:ok, %Activity{} = activity} = ActivityPub.move(old_user, new_user)
|
||||
|
||||
assert %Activity{
|
||||
actor: ^old_ap_id,
|
||||
|
|
@ -1898,7 +2121,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
assert User.following?(follower_move_opted_out, old_user)
|
||||
refute User.following?(follower_move_opted_out, new_user)
|
||||
|
||||
activity = %Activity{activity | object: nil}
|
||||
activity = %{activity | object: nil}
|
||||
|
||||
assert [%Notification{activity: ^activity}] = Notification.for_user(follower)
|
||||
|
||||
|
|
@ -2687,7 +2910,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
assert user.name == " "
|
||||
end
|
||||
|
||||
@tag capture_log: true
|
||||
test "pin_data_from_featured_collection will ignore unsupported values" do
|
||||
assert %{} ==
|
||||
ActivityPub.pin_data_from_featured_collection(%{
|
||||
|
|
@ -2695,4 +2917,38 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
"first" => "https://social.example/users/alice/collections/featured?page=true"
|
||||
})
|
||||
end
|
||||
|
||||
test "fetch_and_prepare_featured_from_ap_id handles embedded first collection pages" do
|
||||
featured_url = "https://social.example/users/alice/collections/featured"
|
||||
first_page_url = "#{featured_url}?page=true"
|
||||
object_url = "https://social.example/objects/1"
|
||||
|
||||
featured_data =
|
||||
Jason.encode!(%{
|
||||
"id" => featured_url,
|
||||
"type" => "OrderedCollection",
|
||||
"first" => %{
|
||||
"id" => first_page_url,
|
||||
"type" => "OrderedCollectionPage",
|
||||
"partOf" => featured_url,
|
||||
"orderedItems" => [object_url]
|
||||
}
|
||||
})
|
||||
|
||||
Tesla.Mock.mock(fn
|
||||
%{method: :get, url: ^featured_url} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: featured_data,
|
||||
headers: [{"content-type", "application/activity+json"}]
|
||||
}
|
||||
end)
|
||||
|
||||
refute capture_log(fn ->
|
||||
assert {:ok, pinned_objects} =
|
||||
ActivityPub.fetch_and_prepare_featured_from_ap_id(featured_url)
|
||||
|
||||
assert Map.has_key?(pinned_objects, object_url)
|
||||
end) =~ "Could not parse featured collection"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicyTest do
|
|||
user = insert(:user)
|
||||
|
||||
{:ok, activity} = CommonAPI.post(user, %{status: "#nsfw hey"})
|
||||
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
|
||||
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
|
||||
|
||||
assert modified["object"]["sensitive"]
|
||||
end
|
||||
|
|
@ -94,7 +94,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicyTest do
|
|||
user = insert(:user)
|
||||
|
||||
{:ok, activity} = CommonAPI.post(user, %{status: "#cofe hey"})
|
||||
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
|
||||
{:ok, modified} = Transmogrifier.prepare_activity(activity.data)
|
||||
|
||||
refute modified["object"]["sensitive"]
|
||||
end
|
||||
|
|
|
|||
|
|
@ -109,4 +109,22 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest do
|
|||
{:ok, filtered} = InlineQuotePolicy.filter(activity)
|
||||
assert filtered == activity
|
||||
end
|
||||
|
||||
# Mastodon uses p tags instead of span in their quote posts
|
||||
# URLs in quoteUri and post content are already mismatched
|
||||
test "skips objects which already have an .inline-quote p" do
|
||||
object = File.read!("test/fixtures/quote_post/mastodon_quote_post.json") |> Jason.decode!()
|
||||
|
||||
# Normally the ObjectValidator will fix this before it reaches MRF
|
||||
object = Map.put(object, "quoteUrl", object["quoteUri"])
|
||||
|
||||
activity = %{
|
||||
"type" => "Create",
|
||||
"actor" => "https://mastodon.social/users/gwynnion",
|
||||
"object" => object
|
||||
}
|
||||
|
||||
{:ok, filtered} = InlineQuotePolicy.filter(activity)
|
||||
assert filtered == activity
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -54,14 +54,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do
|
|||
setup do: clear_config([:media_proxy, :enabled], true)
|
||||
|
||||
test "it prefetches media proxy URIs" do
|
||||
Tesla.Mock.mock(fn %{method: :get, url: "http://example.com/image.jpg"} ->
|
||||
{:ok, %Tesla.Env{status: 200, body: ""}}
|
||||
end)
|
||||
|
||||
with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do
|
||||
with_mock HTTP,
|
||||
get: fn _, _, opts ->
|
||||
send(self(), {:prefetch_opts, opts})
|
||||
{:ok, []}
|
||||
end do
|
||||
MediaProxyWarmingPolicy.filter(@message)
|
||||
|
||||
assert called(HTTP.get(:_, :_, :_))
|
||||
assert_receive {:prefetch_opts, opts}
|
||||
refute Keyword.has_key?(opts, :follow_redirect)
|
||||
refute Keyword.has_key?(opts, :force_redirect)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -81,10 +84,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do
|
|||
end
|
||||
|
||||
test "history-aware" do
|
||||
Tesla.Mock.mock(fn %{method: :get, url: "http://example.com/image.jpg"} ->
|
||||
{:ok, %Tesla.Env{status: 200, body: ""}}
|
||||
end)
|
||||
|
||||
with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do
|
||||
MRF.filter_one(MediaProxyWarmingPolicy, @message_with_history)
|
||||
|
||||
|
|
@ -93,10 +92,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do
|
|||
end
|
||||
|
||||
test "works with Updates" do
|
||||
Tesla.Mock.mock(fn %{method: :get, url: "http://example.com/image.jpg"} ->
|
||||
{:ok, %Tesla.Env{status: 200, body: ""}}
|
||||
end)
|
||||
|
||||
with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do
|
||||
MRF.filter_one(MediaProxyWarmingPolicy, @message_with_history |> Map.put("type", "Update"))
|
||||
|
||||
|
|
|
|||
139
test/pleroma/web/activity_pub/mrf/quiet_reply_test.exs
Normal file
139
test/pleroma/web/activity_pub/mrf/quiet_reply_test.exs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# 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.MRF.QuietReplyTest do
|
||||
use Pleroma.DataCase
|
||||
import Pleroma.Factory
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.MRF.QuietReply
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
test "replying to public post is forced to be quiet" do
|
||||
batman = insert(:user, nickname: "batman")
|
||||
robin = insert(:user, nickname: "robin")
|
||||
|
||||
{:ok, post} = CommonAPI.post(batman, %{status: "To the Batmobile!"})
|
||||
|
||||
reply = %{
|
||||
"type" => "Create",
|
||||
"actor" => robin.ap_id,
|
||||
"to" => [
|
||||
batman.ap_id,
|
||||
Pleroma.Constants.as_public()
|
||||
],
|
||||
"cc" => [robin.follower_address],
|
||||
"object" => %{
|
||||
"type" => "Note",
|
||||
"actor" => robin.ap_id,
|
||||
"content" => "@batman Wait up, I forgot my spandex!",
|
||||
"to" => [
|
||||
batman.ap_id,
|
||||
Pleroma.Constants.as_public()
|
||||
],
|
||||
"cc" => [robin.follower_address],
|
||||
"inReplyTo" => Object.normalize(post).data["id"]
|
||||
}
|
||||
}
|
||||
|
||||
assert {:ok, filtered} = QuietReply.filter(reply)
|
||||
|
||||
assert batman.ap_id in filtered["to"]
|
||||
assert batman.ap_id in filtered["object"]["to"]
|
||||
assert robin.follower_address in filtered["to"]
|
||||
assert robin.follower_address in filtered["object"]["to"]
|
||||
assert Pleroma.Constants.as_public() in filtered["cc"]
|
||||
assert Pleroma.Constants.as_public() in filtered["object"]["cc"]
|
||||
end
|
||||
|
||||
test "replying to unlisted post is unmodified" do
|
||||
batman = insert(:user, nickname: "batman")
|
||||
robin = insert(:user, nickname: "robin")
|
||||
|
||||
{:ok, post} = CommonAPI.post(batman, %{status: "To the Batmobile!", visibility: "private"})
|
||||
|
||||
reply = %{
|
||||
"type" => "Create",
|
||||
"actor" => robin.ap_id,
|
||||
"to" => [batman.ap_id],
|
||||
"cc" => [],
|
||||
"object" => %{
|
||||
"type" => "Note",
|
||||
"actor" => robin.ap_id,
|
||||
"content" => "@batman Wait up, I forgot my spandex!",
|
||||
"to" => [batman.ap_id],
|
||||
"cc" => [],
|
||||
"inReplyTo" => Object.normalize(post).data["id"]
|
||||
}
|
||||
}
|
||||
|
||||
assert {:ok, filtered} = QuietReply.filter(reply)
|
||||
|
||||
assert match?(^filtered, reply)
|
||||
end
|
||||
|
||||
test "replying direct is unmodified" do
|
||||
batman = insert(:user, nickname: "batman")
|
||||
robin = insert(:user, nickname: "robin")
|
||||
|
||||
{:ok, post} = CommonAPI.post(batman, %{status: "To the Batmobile!"})
|
||||
|
||||
reply = %{
|
||||
"type" => "Create",
|
||||
"actor" => robin.ap_id,
|
||||
"to" => [batman.ap_id],
|
||||
"cc" => [],
|
||||
"object" => %{
|
||||
"type" => "Note",
|
||||
"actor" => robin.ap_id,
|
||||
"content" => "@batman Wait up, I forgot my spandex!",
|
||||
"to" => [batman.ap_id],
|
||||
"cc" => [],
|
||||
"inReplyTo" => Object.normalize(post).data["id"]
|
||||
}
|
||||
}
|
||||
|
||||
assert {:ok, filtered} = QuietReply.filter(reply)
|
||||
|
||||
assert match?(^filtered, reply)
|
||||
end
|
||||
|
||||
test "replying followers-only is unmodified" do
|
||||
batman = insert(:user, nickname: "batman")
|
||||
robin = insert(:user, nickname: "robin")
|
||||
|
||||
{:ok, post} = CommonAPI.post(batman, %{status: "To the Batmobile!"})
|
||||
|
||||
reply = %{
|
||||
"type" => "Create",
|
||||
"actor" => robin.ap_id,
|
||||
"to" => [batman.ap_id, robin.follower_address],
|
||||
"cc" => [],
|
||||
"object" => %{
|
||||
"type" => "Note",
|
||||
"actor" => robin.ap_id,
|
||||
"content" => "@batman Wait up, I forgot my spandex!",
|
||||
"to" => [batman.ap_id, robin.follower_address],
|
||||
"cc" => [],
|
||||
"inReplyTo" => Object.normalize(post).data["id"]
|
||||
}
|
||||
}
|
||||
|
||||
assert {:ok, filtered} = QuietReply.filter(reply)
|
||||
|
||||
assert match?(^filtered, reply)
|
||||
end
|
||||
|
||||
test "non-reply posts are unmodified" do
|
||||
batman = insert(:user, nickname: "batman")
|
||||
|
||||
{:ok, post} = CommonAPI.post(batman, %{status: "To the Batmobile!"})
|
||||
|
||||
assert {:ok, filtered} = QuietReply.filter(post)
|
||||
|
||||
assert match?(^filtered, post)
|
||||
end
|
||||
end
|
||||
155
test/pleroma/web/activity_pub/mrf/remote_report_policy_test.exs
Normal file
155
test/pleroma/web/activity_pub/mrf/remote_report_policy_test.exs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
defmodule Pleroma.Web.ActivityPub.MRF.RemoteReportPolicyTest do
|
||||
use Pleroma.DataCase, async: false
|
||||
|
||||
alias Pleroma.Web.ActivityPub.MRF.RemoteReportPolicy
|
||||
|
||||
setup do
|
||||
clear_config([:mrf_remote_report, :reject_all], false)
|
||||
end
|
||||
|
||||
test "doesn't impact local report" do
|
||||
clear_config([:mrf_remote_report, :reject_anonymous], true)
|
||||
clear_config([:mrf_remote_report, :reject_empty_message], true)
|
||||
|
||||
activity = %{
|
||||
"type" => "Flag",
|
||||
"actor" => "http://localhost:4001/actor",
|
||||
"object" => ["https://mastodon.online/users/Gargron"]
|
||||
}
|
||||
|
||||
assert {:ok, _} = RemoteReportPolicy.filter(activity)
|
||||
end
|
||||
|
||||
test "rejects anonymous report if `reject_anonymous: true`" do
|
||||
clear_config([:mrf_remote_report, :reject_anonymous], true)
|
||||
clear_config([:mrf_remote_report, :reject_empty_message], true)
|
||||
|
||||
activity = %{
|
||||
"type" => "Flag",
|
||||
"actor" => "https://mastodon.social/actor",
|
||||
"object" => ["https://mastodon.online/users/Gargron"]
|
||||
}
|
||||
|
||||
assert {:reject, _} = RemoteReportPolicy.filter(activity)
|
||||
end
|
||||
|
||||
test "preserves anonymous report if `reject_anonymous: false`" do
|
||||
clear_config([:mrf_remote_report, :reject_anonymous], false)
|
||||
clear_config([:mrf_remote_report, :reject_empty_message], false)
|
||||
|
||||
activity = %{
|
||||
"type" => "Flag",
|
||||
"actor" => "https://mastodon.social/actor",
|
||||
"object" => ["https://mastodon.online/users/Gargron"]
|
||||
}
|
||||
|
||||
assert {:ok, _} = RemoteReportPolicy.filter(activity)
|
||||
end
|
||||
|
||||
test "rejects report on third party if `reject_third_party: true`" do
|
||||
clear_config([:mrf_remote_report, :reject_third_party], true)
|
||||
clear_config([:mrf_remote_report, :reject_empty_message], false)
|
||||
|
||||
activity = %{
|
||||
"type" => "Flag",
|
||||
"actor" => "https://mastodon.social/users/Gargron",
|
||||
"object" => ["https://mastodon.online/users/Gargron"]
|
||||
}
|
||||
|
||||
assert {:reject, _} = RemoteReportPolicy.filter(activity)
|
||||
end
|
||||
|
||||
test "preserves report on first party if `reject_third_party: true`" do
|
||||
clear_config([:mrf_remote_report, :reject_third_party], true)
|
||||
clear_config([:mrf_remote_report, :reject_empty_message], false)
|
||||
|
||||
activity = %{
|
||||
"type" => "Flag",
|
||||
"actor" => "https://mastodon.social/users/Gargron",
|
||||
"object" => ["http://localhost:4001/actor"]
|
||||
}
|
||||
|
||||
assert {:ok, _} = RemoteReportPolicy.filter(activity)
|
||||
end
|
||||
|
||||
test "preserves report on third party if `reject_third_party: false`" do
|
||||
clear_config([:mrf_remote_report, :reject_third_party], false)
|
||||
clear_config([:mrf_remote_report, :reject_empty_message], false)
|
||||
|
||||
activity = %{
|
||||
"type" => "Flag",
|
||||
"actor" => "https://mastodon.social/users/Gargron",
|
||||
"object" => ["https://mastodon.online/users/Gargron"]
|
||||
}
|
||||
|
||||
assert {:ok, _} = RemoteReportPolicy.filter(activity)
|
||||
end
|
||||
|
||||
test "rejects empty message report if `reject_empty_message: true`" do
|
||||
clear_config([:mrf_remote_report, :reject_anonymous], false)
|
||||
clear_config([:mrf_remote_report, :reject_empty_message], true)
|
||||
|
||||
activity = %{
|
||||
"type" => "Flag",
|
||||
"actor" => "https://mastodon.social/users/Gargron",
|
||||
"object" => ["https://mastodon.online/users/Gargron"]
|
||||
}
|
||||
|
||||
assert {:reject, _} = RemoteReportPolicy.filter(activity)
|
||||
end
|
||||
|
||||
test "rejects empty message report (\"\") if `reject_empty_message: true`" do
|
||||
clear_config([:mrf_remote_report, :reject_anonymous], false)
|
||||
clear_config([:mrf_remote_report, :reject_empty_message], true)
|
||||
|
||||
activity = %{
|
||||
"type" => "Flag",
|
||||
"actor" => "https://mastodon.social/users/Gargron",
|
||||
"object" => ["https://mastodon.online/users/Gargron"],
|
||||
"content" => ""
|
||||
}
|
||||
|
||||
assert {:reject, _} = RemoteReportPolicy.filter(activity)
|
||||
end
|
||||
|
||||
test "preserves empty message report if `reject_empty_message: false`" do
|
||||
clear_config([:mrf_remote_report, :reject_anonymous], false)
|
||||
clear_config([:mrf_remote_report, :reject_empty_message], false)
|
||||
|
||||
activity = %{
|
||||
"type" => "Flag",
|
||||
"actor" => "https://mastodon.social/users/Gargron",
|
||||
"object" => ["https://mastodon.online/users/Gargron"]
|
||||
}
|
||||
|
||||
assert {:ok, _} = RemoteReportPolicy.filter(activity)
|
||||
end
|
||||
|
||||
test "preserves anonymous, empty message report with all settings disabled" do
|
||||
clear_config([:mrf_remote_report, :reject_anonymous], false)
|
||||
clear_config([:mrf_remote_report, :reject_empty_message], false)
|
||||
|
||||
activity = %{
|
||||
"type" => "Flag",
|
||||
"actor" => "https://mastodon.social/actor",
|
||||
"object" => ["https://mastodon.online/users/Gargron"]
|
||||
}
|
||||
|
||||
assert {:ok, _} = RemoteReportPolicy.filter(activity)
|
||||
end
|
||||
|
||||
test "reject remote report if `reject_all: true`" do
|
||||
clear_config([:mrf_remote_report, :reject_all], true)
|
||||
clear_config([:mrf_remote_report, :reject_anonymous], false)
|
||||
clear_config([:mrf_remote_report, :reject_empty_message], false)
|
||||
|
||||
activity = %{
|
||||
"type" => "Flag",
|
||||
"actor" => "https://mastodon.social/users/Gargron",
|
||||
"content" => "Transphobia",
|
||||
"object" => ["https://mastodon.online/users/Gargron"]
|
||||
}
|
||||
|
||||
assert {:reject, _} = RemoteReportPolicy.filter(activity)
|
||||
end
|
||||
end
|
||||
|
|
@ -252,6 +252,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
|||
remote_message = build_remote_message()
|
||||
|
||||
assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
|
||||
assert SimplePolicy.id_filter(remote_message["actor"])
|
||||
end
|
||||
|
||||
test "activity has a matching host" do
|
||||
|
|
@ -260,6 +261,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
|||
remote_message = build_remote_message()
|
||||
|
||||
assert {:reject, _} = SimplePolicy.filter(remote_message)
|
||||
refute SimplePolicy.id_filter(remote_message["actor"])
|
||||
end
|
||||
|
||||
test "activity matches with wildcard domain" do
|
||||
|
|
@ -268,6 +270,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
|||
remote_message = build_remote_message()
|
||||
|
||||
assert {:reject, _} = SimplePolicy.filter(remote_message)
|
||||
refute SimplePolicy.id_filter(remote_message["actor"])
|
||||
end
|
||||
|
||||
test "actor has a matching host" do
|
||||
|
|
@ -276,6 +279,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
|||
remote_user = build_remote_user()
|
||||
|
||||
assert {:reject, _} = SimplePolicy.filter(remote_user)
|
||||
refute SimplePolicy.id_filter(remote_user["id"])
|
||||
end
|
||||
|
||||
test "reject Announce when object would be rejected" do
|
||||
|
|
@ -288,6 +292,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
|||
}
|
||||
|
||||
assert {:reject, _} = SimplePolicy.filter(announce)
|
||||
# Note: Non-Applicable for id_filter/1
|
||||
end
|
||||
|
||||
test "reject by URI object" do
|
||||
|
|
@ -300,6 +305,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
|||
}
|
||||
|
||||
assert {:reject, _} = SimplePolicy.filter(announce)
|
||||
# Note: Non-Applicable for id_filter/1
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -318,7 +324,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
|||
following_user = insert(:user)
|
||||
non_following_user = insert(:user)
|
||||
|
||||
{:ok, _, _, _} = CommonAPI.follow(following_user, actor)
|
||||
{:ok, _, _, _} = CommonAPI.follow(actor, following_user)
|
||||
|
||||
activity = %{
|
||||
"actor" => actor.ap_id,
|
||||
|
|
@ -370,6 +376,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
|||
|
||||
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
||||
assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
|
||||
assert SimplePolicy.id_filter(local_message["actor"])
|
||||
assert SimplePolicy.id_filter(remote_message["actor"])
|
||||
end
|
||||
|
||||
test "is not empty but activity doesn't have a matching host" do
|
||||
|
|
@ -380,6 +388,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
|||
|
||||
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
||||
assert {:reject, _} = SimplePolicy.filter(remote_message)
|
||||
assert SimplePolicy.id_filter(local_message["actor"])
|
||||
refute SimplePolicy.id_filter(remote_message["actor"])
|
||||
end
|
||||
|
||||
test "activity has a matching host" do
|
||||
|
|
@ -390,6 +400,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
|||
|
||||
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
||||
assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
|
||||
assert SimplePolicy.id_filter(local_message["actor"])
|
||||
assert SimplePolicy.id_filter(remote_message["actor"])
|
||||
end
|
||||
|
||||
test "activity matches with wildcard domain" do
|
||||
|
|
@ -400,6 +412,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
|||
|
||||
assert SimplePolicy.filter(local_message) == {:ok, local_message}
|
||||
assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
|
||||
assert SimplePolicy.id_filter(local_message["actor"])
|
||||
assert SimplePolicy.id_filter(remote_message["actor"])
|
||||
end
|
||||
|
||||
test "actor has a matching host" do
|
||||
|
|
@ -408,6 +422,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
|
|||
remote_user = build_remote_user()
|
||||
|
||||
assert SimplePolicy.filter(remote_user) == {:ok, remote_user}
|
||||
assert SimplePolicy.id_filter(remote_user["id"])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do
|
|||
assert File.exists?(fullpath)
|
||||
end
|
||||
|
||||
test "rejects invalid shortcodes", %{path: path} do
|
||||
test "rejects invalid shortcodes with slashes", %{path: path} do
|
||||
message = %{
|
||||
"type" => "Create",
|
||||
"object" => %{
|
||||
|
|
@ -113,6 +113,58 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do
|
|||
refute File.exists?(fullpath)
|
||||
end
|
||||
|
||||
test "rejects invalid shortcodes with dots", %{path: path} do
|
||||
message = %{
|
||||
"type" => "Create",
|
||||
"object" => %{
|
||||
"emoji" => [{"fired.fox", "https://example.org/emoji/firedfox"}],
|
||||
"actor" => "https://example.org/users/admin"
|
||||
}
|
||||
}
|
||||
|
||||
fullpath = Path.join(path, "fired.fox.png")
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox"} ->
|
||||
%Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
|
||||
end)
|
||||
|
||||
clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
|
||||
|
||||
refute "fired.fox" in installed()
|
||||
refute File.exists?(path)
|
||||
|
||||
assert {:ok, _message} = StealEmojiPolicy.filter(message)
|
||||
|
||||
refute "fired.fox" in installed()
|
||||
refute File.exists?(fullpath)
|
||||
end
|
||||
|
||||
test "rejects invalid shortcodes with special characters", %{path: path} do
|
||||
message = %{
|
||||
"type" => "Create",
|
||||
"object" => %{
|
||||
"emoji" => [{"fired:fox", "https://example.org/emoji/firedfox"}],
|
||||
"actor" => "https://example.org/users/admin"
|
||||
}
|
||||
}
|
||||
|
||||
fullpath = Path.join(path, "fired:fox.png")
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox"} ->
|
||||
%Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
|
||||
end)
|
||||
|
||||
clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
|
||||
|
||||
refute "fired:fox" in installed()
|
||||
refute File.exists?(path)
|
||||
|
||||
assert {:ok, _message} = StealEmojiPolicy.filter(message)
|
||||
|
||||
refute "fired:fox" in installed()
|
||||
refute File.exists?(fullpath)
|
||||
end
|
||||
|
||||
test "reject regex shortcode", %{message: message} do
|
||||
refute "firedfox" in installed()
|
||||
|
||||
|
|
@ -171,5 +223,74 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do
|
|||
refute "firedfox" in installed()
|
||||
end
|
||||
|
||||
test "accepts valid alphanum shortcodes", %{path: path} do
|
||||
message = %{
|
||||
"type" => "Create",
|
||||
"object" => %{
|
||||
"emoji" => [{"fire1fox", "https://example.org/emoji/fire1fox.png"}],
|
||||
"actor" => "https://example.org/users/admin"
|
||||
}
|
||||
}
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/fire1fox.png"} ->
|
||||
%Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
|
||||
end)
|
||||
|
||||
clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
|
||||
|
||||
refute "fire1fox" in installed()
|
||||
refute File.exists?(path)
|
||||
|
||||
assert {:ok, _message} = StealEmojiPolicy.filter(message)
|
||||
|
||||
assert "fire1fox" in installed()
|
||||
end
|
||||
|
||||
test "accepts valid shortcodes with underscores", %{path: path} do
|
||||
message = %{
|
||||
"type" => "Create",
|
||||
"object" => %{
|
||||
"emoji" => [{"fire_fox", "https://example.org/emoji/fire_fox.png"}],
|
||||
"actor" => "https://example.org/users/admin"
|
||||
}
|
||||
}
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/fire_fox.png"} ->
|
||||
%Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
|
||||
end)
|
||||
|
||||
clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
|
||||
|
||||
refute "fire_fox" in installed()
|
||||
refute File.exists?(path)
|
||||
|
||||
assert {:ok, _message} = StealEmojiPolicy.filter(message)
|
||||
|
||||
assert "fire_fox" in installed()
|
||||
end
|
||||
|
||||
test "accepts valid shortcodes with hyphens", %{path: path} do
|
||||
message = %{
|
||||
"type" => "Create",
|
||||
"object" => %{
|
||||
"emoji" => [{"fire-fox", "https://example.org/emoji/fire-fox.png"}],
|
||||
"actor" => "https://example.org/users/admin"
|
||||
}
|
||||
}
|
||||
|
||||
Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/fire-fox.png"} ->
|
||||
%Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
|
||||
end)
|
||||
|
||||
clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
|
||||
|
||||
refute "fire-fox" in installed()
|
||||
refute File.exists?(path)
|
||||
|
||||
assert {:ok, _message} = StealEmojiPolicy.filter(message)
|
||||
|
||||
assert "fire-fox" in installed()
|
||||
end
|
||||
|
||||
defp installed, do: Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,18 +10,25 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
|
|||
|
||||
alias Pleroma.Web.ActivityPub.MRF
|
||||
|
||||
defp regexes_match!([], []), do: true
|
||||
|
||||
defp regexes_match!([authority | authority_rest], [checked | checked_rest]) do
|
||||
authority.source == checked.source and regexes_match!(authority_rest, checked_rest)
|
||||
end
|
||||
|
||||
defp regexes_match!(_, _), do: false
|
||||
|
||||
test "subdomains_regex/1" do
|
||||
assert MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"]) == [
|
||||
~r/^unsafe.tld$/i,
|
||||
~r/^(.*\.)*unsafe.tld$/i
|
||||
]
|
||||
regexes = MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"])
|
||||
|
||||
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 == [~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")
|
||||
|
|
@ -32,7 +39,7 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
|
|||
test "wildcard domains with one subdomain" do
|
||||
regexes = MRF.subdomains_regex(["*.unsafe.tld"])
|
||||
|
||||
assert 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")
|
||||
|
|
@ -43,7 +50,7 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
|
|||
test "wildcard domains with two subdomains" do
|
||||
regexes = MRF.subdomains_regex(["*.unsafe.tld"])
|
||||
|
||||
assert 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")
|
||||
|
|
@ -54,7 +61,7 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
|
|||
test "matches are case-insensitive" do
|
||||
regexes = MRF.subdomains_regex(["UnSafe.TLD", "UnSAFE2.Tld"])
|
||||
|
||||
assert 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")
|
||||
|
|
@ -65,7 +72,6 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
|
|||
refute MRF.subdomain_match?(regexes, "example.com")
|
||||
end
|
||||
|
||||
@tag capture_log: true
|
||||
test "logs sensible error on accidental wildcard" do
|
||||
assert_raise Regex.CompileError, fn ->
|
||||
assert capture_log(MRF.subdomains_regex(["*unsafe.tld"])) =~
|
||||
|
|
|
|||
|
|
@ -86,23 +86,32 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidationTest do
|
|||
object = Object.normalize(post_activity, fetch: false)
|
||||
|
||||
# Another user can't announce it
|
||||
{:ok, announce, []} = Builder.announce(announcer, object, public: false)
|
||||
{:ok, announce, []} = Builder.announce(announcer, object, visibility: "private")
|
||||
|
||||
{:error, cng} = ObjectValidator.validate(announce, [])
|
||||
|
||||
assert {:actor, {"can not announce this object", []}} in cng.errors
|
||||
|
||||
# The actor of the object can announce it
|
||||
{:ok, announce, []} = Builder.announce(user, object, public: false)
|
||||
# The actor of the object can announce it with a restrictive scope
|
||||
{:ok, announce, []} = Builder.announce(user, object, visibility: "private")
|
||||
assert {:ok, _, _} = ObjectValidator.validate(announce, [])
|
||||
|
||||
{:ok, announce, []} = Builder.announce(user, object, visibility: "direct")
|
||||
assert {:ok, _, _} = ObjectValidator.validate(announce, [])
|
||||
|
||||
# The actor of the object can not announce it publicly
|
||||
{:ok, announce, []} = Builder.announce(user, object, public: true)
|
||||
{:ok, announce, []} = Builder.announce(user, object, visibility: "public")
|
||||
{:error, cng1} = ObjectValidator.validate(announce, [])
|
||||
|
||||
{:error, cng} = ObjectValidator.validate(announce, [])
|
||||
{:ok, announce, []} = Builder.announce(user, object, visibility: "unlisted")
|
||||
{:error, cng2} = ObjectValidator.validate(announce, [])
|
||||
|
||||
assert {:actor, {"can not announce this object publicly", []}} in cng.errors
|
||||
{:ok, announce, []} = Builder.announce(user, object, visibility: "local")
|
||||
{:error, cng3} = ObjectValidator.validate(announce, [])
|
||||
|
||||
for cng <- [cng1, cng2, cng3] do
|
||||
assert {:actor, {"can not announce this object publicly", []}} in cng.errors
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,12 +5,33 @@
|
|||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.Language.LanguageDetectorMock
|
||||
alias Pleroma.StaticStubbedConfigMock
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
|
||||
import Mox
|
||||
import Pleroma.Factory
|
||||
|
||||
# Setup for all tests
|
||||
setup do
|
||||
# Stub the StaticStubbedConfigMock to return our mock for the provider
|
||||
StaticStubbedConfigMock
|
||||
|> stub(:get, fn
|
||||
[Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock
|
||||
_other -> nil
|
||||
end)
|
||||
|
||||
# Stub the LanguageDetectorMock with default implementations
|
||||
LanguageDetectorMock
|
||||
|> stub(:missing_dependencies, fn -> [] end)
|
||||
|> stub(:configured?, fn -> true end)
|
||||
|> stub(:detect, fn _text -> nil end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
describe "Notes" do
|
||||
setup do
|
||||
user = insert(:user)
|
||||
|
|
@ -43,10 +64,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
|
|||
setup do
|
||||
user = insert(:user)
|
||||
{:ok, activity} = Pleroma.Web.CommonAPI.post(user, %{status: "mew mew :dinosaur:"})
|
||||
{:ok, edit} = Pleroma.Web.CommonAPI.update(user, activity, %{status: "edited :blank:"})
|
||||
{:ok, edit} = Pleroma.Web.CommonAPI.update(activity, user, %{status: "edited :blank:"})
|
||||
|
||||
{:ok, %{"object" => external_rep}} =
|
||||
Pleroma.Web.ActivityPub.Transmogrifier.prepare_outgoing(edit.data)
|
||||
Pleroma.Web.ActivityPub.Transmogrifier.prepare_activity(edit.data)
|
||||
|
||||
%{external_rep: external_rep}
|
||||
end
|
||||
|
|
@ -128,6 +149,182 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
|
|||
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
|
||||
end
|
||||
|
||||
test "a Misskey MFM note is rendered from source content" do
|
||||
user = insert(:user, ap_id: "https://misskey.example/users/alice")
|
||||
|
||||
note = %{
|
||||
"id" => "https://misskey.example/notes/1",
|
||||
"type" => "Note",
|
||||
"actor" => user.ap_id,
|
||||
"attributedTo" => user.ap_id,
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => [],
|
||||
"content" => "original content",
|
||||
"context" => Utils.generate_context_id(),
|
||||
"source" => %{
|
||||
"content" => "$[spin.speed=1s mfm goes here] <script>alert('xss')</script>",
|
||||
"mediaType" => "text/x.misskeymarkdown"
|
||||
}
|
||||
}
|
||||
|
||||
%{valid?: true, changes: %{content: content, source: source}} =
|
||||
ArticleNotePageValidator.cast_and_validate(note)
|
||||
|
||||
assert source["mediaType"] == "text/x.misskeymarkdown"
|
||||
assert content =~ ~s(class="mfm-spin")
|
||||
assert content =~ ~s(data-mfm-speed="1s")
|
||||
assert content =~ "mfm goes here"
|
||||
refute content =~ "original content"
|
||||
refute content =~ "<script"
|
||||
end
|
||||
|
||||
test "a Misskey MFM note resolves only cached AP mention tags" do
|
||||
remote_user = insert(:user, ap_id: "https://misskey.example/users/carol")
|
||||
local_user = insert(:user, nickname: "local_user")
|
||||
|
||||
note = %{
|
||||
"id" => "https://misskey.example/notes/3",
|
||||
"type" => "Note",
|
||||
"actor" => remote_user.ap_id,
|
||||
"attributedTo" => remote_user.ap_id,
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => [],
|
||||
"content" => "original content",
|
||||
"context" => Utils.generate_context_id(),
|
||||
"tag" => [
|
||||
%{
|
||||
"type" => "Mention",
|
||||
"name" => "@local_user",
|
||||
"href" => local_user.ap_id
|
||||
},
|
||||
%{
|
||||
"type" => "Mention",
|
||||
"name" => "@uncached",
|
||||
"href" => "https://misskey.example/users/uncached"
|
||||
}
|
||||
],
|
||||
"source" => %{
|
||||
"content" => "@local_user @uncached $[spin hello]",
|
||||
"mediaType" => "text/x.misskeymarkdown"
|
||||
}
|
||||
}
|
||||
|
||||
%{valid?: true, changes: %{content: content}} =
|
||||
ArticleNotePageValidator.cast_and_validate(note)
|
||||
|
||||
assert content =~ local_user.ap_id
|
||||
assert content =~ "@uncached"
|
||||
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")
|
||||
|
||||
note = %{
|
||||
"id" => "https://misskey.example/notes/4",
|
||||
"type" => "Note",
|
||||
"actor" => user.ap_id,
|
||||
"attributedTo" => user.ap_id,
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => [],
|
||||
"content" => "<span class=\"mfm-spin\">safe fallback</span>",
|
||||
"context" => Utils.generate_context_id(),
|
||||
"source" => %{
|
||||
"content" => String.duplicate("x", 5_001),
|
||||
"mediaType" => "text/x.misskeymarkdown"
|
||||
}
|
||||
}
|
||||
|
||||
%{valid?: true, changes: %{content: content, source: source}} =
|
||||
ArticleNotePageValidator.cast_and_validate(note)
|
||||
|
||||
assert content == "<span class=\"mfm-spin\">safe fallback</span>"
|
||||
refute Map.has_key?(source, "content")
|
||||
end
|
||||
|
||||
test "a note drops oversized non-MFM source content" do
|
||||
user = insert(:user, ap_id: "https://example.com/users/source")
|
||||
|
||||
note = %{
|
||||
"id" => "https://example.com/notes/1",
|
||||
"type" => "Note",
|
||||
"actor" => user.ap_id,
|
||||
"attributedTo" => user.ap_id,
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => [],
|
||||
"content" => "regular content",
|
||||
"context" => Utils.generate_context_id(),
|
||||
"source" => %{
|
||||
"content" => String.duplicate("x", 5_001),
|
||||
"mediaType" => "text/markdown"
|
||||
}
|
||||
}
|
||||
|
||||
%{valid?: true, changes: %{source: source}} = ArticleNotePageValidator.cast_and_validate(note)
|
||||
|
||||
assert source == %{"mediaType" => "text/markdown"}
|
||||
end
|
||||
|
||||
test "a Misskey MFM note with legacy _misskey_content is rendered" do
|
||||
user = insert(:user, ap_id: "https://misskey.example/users/legacy")
|
||||
|
||||
note = %{
|
||||
"id" => "https://misskey.example/notes/5",
|
||||
"type" => "Note",
|
||||
"actor" => user.ap_id,
|
||||
"attributedTo" => user.ap_id,
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => [],
|
||||
"content" => "original content",
|
||||
"context" => Utils.generate_context_id(),
|
||||
"_misskey_content" => "$[spin legacy]"
|
||||
}
|
||||
|
||||
%{valid?: true, changes: %{content: content, source: source}} =
|
||||
ArticleNotePageValidator.cast_and_validate(note)
|
||||
|
||||
assert source == %{"content" => "$[spin legacy]", "mediaType" => "text/x.misskeymarkdown"}
|
||||
assert content =~ ~s(class="mfm-spin")
|
||||
assert content =~ "legacy"
|
||||
end
|
||||
|
||||
test "a Misskey MFM note with htmlMfm is scrubbed but not rendered from source content" do
|
||||
user = insert(:user, ap_id: "https://misskey.example/users/bob")
|
||||
|
||||
note = %{
|
||||
"id" => "https://misskey.example/notes/2",
|
||||
"type" => "Note",
|
||||
"actor" => user.ap_id,
|
||||
"attributedTo" => user.ap_id,
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => [],
|
||||
"content" =>
|
||||
"<span class=\"mfm-spin\">already rendered</span><script>alert('xss')</script>",
|
||||
"htmlMfm" => true,
|
||||
"context" => Utils.generate_context_id(),
|
||||
"source" => %{
|
||||
"content" => String.duplicate("x", 5_001),
|
||||
"mediaType" => "text/x.misskeymarkdown"
|
||||
}
|
||||
}
|
||||
|
||||
%{valid?: true, changes: %{content: content, htmlMfm: true, source: source}} =
|
||||
ArticleNotePageValidator.cast_and_validate(note)
|
||||
|
||||
assert content == "<span class=\"mfm-spin\">already rendered</span>alert('xss')"
|
||||
refute Map.has_key?(source, "content")
|
||||
end
|
||||
|
||||
test "a Note with validated likes collection validates" do
|
||||
insert(:user, ap_id: "https://pol.social/users/mkljczk")
|
||||
|
||||
%{"object" => note} =
|
||||
"test/fixtures/mastodon-update-with-likes.json"
|
||||
|> File.read!()
|
||||
|> Jason.decode!()
|
||||
|
||||
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
|
||||
end
|
||||
|
||||
test "Fedibird quote post" do
|
||||
insert(:user, ap_id: "https://fedibird.com/users/noellabo")
|
||||
|
||||
|
|
@ -176,4 +373,102 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
|
|||
name: "RE: https://server.example/objects/123"
|
||||
}
|
||||
end
|
||||
|
||||
describe "Note language" do
|
||||
test "it detects language from JSON-LD context" do
|
||||
user = insert(:user)
|
||||
|
||||
note_activity = %{
|
||||
"@context" => ["https://www.w3.org/ns/activitystreams", %{"@language" => "pl"}],
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => [],
|
||||
"type" => "Create",
|
||||
"object" => %{
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => [],
|
||||
"id" => Utils.generate_object_id(),
|
||||
"type" => "Note",
|
||||
"content" => "Szczęść Boże",
|
||||
"attributedTo" => user.ap_id
|
||||
},
|
||||
"actor" => user.ap_id
|
||||
}
|
||||
|
||||
{:ok, _create_activity, meta} = ObjectValidator.validate(note_activity, [])
|
||||
|
||||
assert meta[:object_data]["language"] == "pl"
|
||||
end
|
||||
|
||||
test "it detects language from contentMap" do
|
||||
user = insert(:user)
|
||||
|
||||
note = %{
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => [],
|
||||
"id" => Utils.generate_object_id(),
|
||||
"type" => "Note",
|
||||
"content" => "Szczęść Boże",
|
||||
"contentMap" => %{
|
||||
"de" => "Gott segne",
|
||||
"pl" => "Szczęść Boże"
|
||||
},
|
||||
"attributedTo" => user.ap_id
|
||||
}
|
||||
|
||||
{:ok, object} = ArticleNotePageValidator.cast_and_apply(note)
|
||||
|
||||
assert object.language == "pl"
|
||||
end
|
||||
|
||||
test "it doesn't call LanguageDetector when language is specified" do
|
||||
# Set up expectation that detect should not be called
|
||||
LanguageDetectorMock
|
||||
|> expect(:detect, 0, fn _ -> flunk("LanguageDetector.detect should not be called") end)
|
||||
|> stub(:missing_dependencies, fn -> [] end)
|
||||
|> stub(:configured?, fn -> true end)
|
||||
|
||||
# Stub the StaticStubbedConfigMock to return our mock for the provider
|
||||
StaticStubbedConfigMock
|
||||
|> stub(:get, fn
|
||||
[Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock
|
||||
_other -> nil
|
||||
end)
|
||||
|
||||
user = insert(:user)
|
||||
|
||||
note = %{
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => [],
|
||||
"id" => Utils.generate_object_id(),
|
||||
"type" => "Note",
|
||||
"content" => "a post in English",
|
||||
"contentMap" => %{
|
||||
"en" => "a post in English"
|
||||
},
|
||||
"attributedTo" => user.ap_id
|
||||
}
|
||||
|
||||
ArticleNotePageValidator.cast_and_apply(note)
|
||||
end
|
||||
|
||||
test "it adds contentMap if language is specified" do
|
||||
user = insert(:user)
|
||||
|
||||
note = %{
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => [],
|
||||
"id" => Utils.generate_object_id(),
|
||||
"type" => "Note",
|
||||
"content" => "тест",
|
||||
"language" => "uk",
|
||||
"attributedTo" => user.ap_id
|
||||
}
|
||||
|
||||
{:ok, object} = ArticleNotePageValidator.cast_and_apply(note)
|
||||
|
||||
assert object.contentMap == %{
|
||||
"uk" => "тест"
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,6 +13,23 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidatorTest do
|
|||
import Pleroma.Factory
|
||||
|
||||
describe "attachments" do
|
||||
test "works with apng" do
|
||||
attachment =
|
||||
%{
|
||||
"mediaType" => "image/apng",
|
||||
"name" => "",
|
||||
"type" => "Document",
|
||||
"url" =>
|
||||
"https://media.misskeyusercontent.com/io/2859c26e-cd43-4550-848b-b6243bc3fe28.apng"
|
||||
}
|
||||
|
||||
assert {:ok, attachment} =
|
||||
AttachmentValidator.cast_and_validate(attachment)
|
||||
|> Ecto.Changeset.apply_action(:insert)
|
||||
|
||||
assert attachment.mediaType == "image/apng"
|
||||
end
|
||||
|
||||
test "fails without url" do
|
||||
attachment = %{
|
||||
"mediaType" => "",
|
||||
|
|
|
|||
|
|
@ -59,4 +59,37 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidatorTest do
|
|||
assert validated.valid?
|
||||
assert {:context, note["context"]} in validated.changes
|
||||
end
|
||||
|
||||
test "a Create/Note without addressing falls back to the Note's recipients" do
|
||||
user = insert(:user)
|
||||
|
||||
note = %{
|
||||
"id" => Utils.generate_object_id(),
|
||||
"type" => "Note",
|
||||
"actor" => user.ap_id,
|
||||
"to" => [user.follower_address],
|
||||
"cc" => [],
|
||||
"content" => "Hello world",
|
||||
"context" => Utils.generate_context_id()
|
||||
}
|
||||
|
||||
note_activity = %{
|
||||
"id" => Utils.generate_activity_id(),
|
||||
"type" => "Create",
|
||||
"actor" => note["actor"],
|
||||
"object" => note,
|
||||
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
|
||||
"context" => Utils.generate_context_id()
|
||||
}
|
||||
|
||||
# Build metadata
|
||||
{:ok, object_data} = ObjectValidator.cast_and_apply(note_activity["object"])
|
||||
meta = [object_data: ObjectValidator.stringify_keys(object_data)]
|
||||
|
||||
validated = CreateGenericValidator.cast_and_validate(note_activity, meta)
|
||||
|
||||
assert validated.valid?
|
||||
assert Ecto.Changeset.get_field(validated, :to) == note["to"]
|
||||
assert Ecto.Changeset.get_field(validated, :cc) == note["cc"]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -22,5 +22,15 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.FollowValidationTest do
|
|||
test "validates a basic follow object", %{valid_follow: valid_follow} do
|
||||
assert {:ok, _follow, []} = ObjectValidator.validate(valid_follow, [])
|
||||
end
|
||||
|
||||
test "supports a nil cc", %{valid_follow: valid_follow} do
|
||||
valid_follow_with_nil_cc = Map.put(valid_follow, "cc", nil)
|
||||
assert {:ok, _follow, []} = ObjectValidator.validate(valid_follow_with_nil_cc, [])
|
||||
end
|
||||
|
||||
test "supports an empty cc", %{valid_follow: valid_follow} do
|
||||
valid_follow_with_empty_cc = Map.put(valid_follow, "cc", [])
|
||||
assert {:ok, _follow, []} = ObjectValidator.validate(valid_follow_with_empty_cc, [])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidationTest do
|
|||
user: user,
|
||||
post_activity: post_activity
|
||||
} do
|
||||
_like = CommonAPI.favorite(user, post_activity.id)
|
||||
_like = CommonAPI.favorite(post_activity.id, user)
|
||||
|
||||
refute LikeValidator.cast_and_validate(valid_like).valid?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoHandlingTest do
|
|||
setup do
|
||||
user = insert(:user)
|
||||
{:ok, post_activity} = CommonAPI.post(user, %{status: "uguu"})
|
||||
{:ok, like} = CommonAPI.favorite(user, post_activity.id)
|
||||
{:ok, like} = CommonAPI.favorite(post_activity.id, user)
|
||||
{:ok, valid_like_undo, []} = Builder.undo(user, like)
|
||||
|
||||
%{user: user, like: like, valid_like_undo: valid_like_undo}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do
|
|||
assert {:ok, _update, []} = ObjectValidator.validate(valid_update, [])
|
||||
end
|
||||
|
||||
test "returns an error if the object can't be updated by the actor", %{
|
||||
test "returns an error if the object can't be updated by the actor (different domain)", %{
|
||||
valid_update: valid_update
|
||||
} do
|
||||
other_user = insert(:user, local: false)
|
||||
|
|
@ -41,27 +41,72 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do
|
|||
assert {:error, _cng} = ObjectValidator.validate(update, [])
|
||||
end
|
||||
|
||||
test "validates as long as the object is same-origin with the actor", %{
|
||||
test "returns an error if the object can't be updated by the actor (same domain)", %{
|
||||
user: user,
|
||||
valid_update: valid_update
|
||||
} do
|
||||
other_user = insert(:user)
|
||||
user_ap_id = user.ap_id
|
||||
user_domain = URI.parse(user_ap_id).host
|
||||
other_user = insert(:user, local: false, domain: user_domain)
|
||||
|
||||
update =
|
||||
valid_update
|
||||
|> Map.put("actor", other_user.ap_id)
|
||||
|
||||
assert {:ok, _update, []} = ObjectValidator.validate(update, [])
|
||||
assert {:error, _cng} = ObjectValidator.validate(update, [])
|
||||
end
|
||||
|
||||
test "validates if the object is not of an Actor type" do
|
||||
note = insert(:note)
|
||||
test "validates if the object is not of an Actor type", %{user: user} do
|
||||
note = insert(:note, user: user)
|
||||
updated_note = note.data |> Map.put("content", "edited content")
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, update, _} = Builder.update(other_user, updated_note)
|
||||
{:ok, update, _} = Builder.update(user, updated_note)
|
||||
|
||||
assert {:ok, _update, _} = ObjectValidator.validate(update, [])
|
||||
end
|
||||
|
||||
test "returns an error if the remote update target is unknown" do
|
||||
remote_user = insert(:user, local: false, ap_id: "https://example.com/users/alice")
|
||||
|
||||
update = %{
|
||||
"type" => "Update",
|
||||
"actor" => remote_user.ap_id,
|
||||
"id" => "https://example.com/activities/update-unknown-object",
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => [],
|
||||
"object" => %{
|
||||
"type" => "Note",
|
||||
"id" => "https://example.com/objects/unknown",
|
||||
"actor" => remote_user.ap_id,
|
||||
"content" => "edited content",
|
||||
"published" => "2024-07-25T13:33:31Z",
|
||||
"updated" => "2024-07-25T13:34:31Z",
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => []
|
||||
}
|
||||
}
|
||||
|
||||
assert {:error, %Ecto.Changeset{} = cng} = ObjectValidator.validate(update, local: false)
|
||||
refute cng.valid?
|
||||
assert Keyword.has_key?(cng.errors, :object)
|
||||
end
|
||||
|
||||
test "returns an error if the remote update target IRI is unknown" do
|
||||
remote_user = insert(:user, local: false, ap_id: "https://example.com/users/alice")
|
||||
|
||||
update = %{
|
||||
"type" => "Update",
|
||||
"actor" => remote_user.ap_id,
|
||||
"id" => "https://example.com/activities/update-unknown-object-iri",
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => [],
|
||||
"object" => "https://example.com/objects/unknown-iri"
|
||||
}
|
||||
|
||||
assert {:error, %Ecto.Changeset{} = cng} = ObjectValidator.validate(update, local: false)
|
||||
refute cng.valid?
|
||||
assert Keyword.has_key?(cng.errors, :object)
|
||||
end
|
||||
end
|
||||
|
||||
describe "update note" do
|
||||
|
|
@ -132,8 +177,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do
|
|||
setup do
|
||||
user = insert(:user)
|
||||
{:ok, activity} = Pleroma.Web.CommonAPI.post(user, %{status: "mew mew :dinosaur:"})
|
||||
{:ok, edit} = Pleroma.Web.CommonAPI.update(user, activity, %{status: "edited :blank:"})
|
||||
{:ok, external_rep} = Pleroma.Web.ActivityPub.Transmogrifier.prepare_outgoing(edit.data)
|
||||
{:ok, edit} = Pleroma.Web.CommonAPI.update(activity, user, %{status: "edited :blank:"})
|
||||
{:ok, external_rep} = Pleroma.Web.ActivityPub.Transmogrifier.prepare_activity(edit.data)
|
||||
%{external_rep: external_rep}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -3,17 +3,18 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.PublisherTest do
|
||||
use Oban.Testing, repo: Pleroma.Repo
|
||||
use Pleroma.Web.ConnCase
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
import Pleroma.Factory
|
||||
import Tesla.Mock
|
||||
import Mock
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Instances
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Tests.ObanHelpers
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Publisher
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
@as_public "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
|
@ -137,6 +138,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
|
|||
test "publish to url with with different ports" do
|
||||
inbox80 = "http://42.site/users/nick1/inbox"
|
||||
inbox42 = "http://42.site:42/users/nick1/inbox"
|
||||
activity = insert(:note_activity)
|
||||
|
||||
mock(fn
|
||||
%{method: :post, url: "http://42.site:42/users/nick1/inbox"} ->
|
||||
|
|
@ -146,144 +148,28 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
|
|||
{:ok, %Tesla.Env{status: 200, body: "port 80"}}
|
||||
end)
|
||||
|
||||
actor = insert(:user)
|
||||
_actor = insert(:user)
|
||||
|
||||
assert {:ok, %{body: "port 42"}} =
|
||||
Publisher.publish_one(%{
|
||||
Publisher.prepare_one(%{
|
||||
inbox: inbox42,
|
||||
json: "{}",
|
||||
actor: actor,
|
||||
id: 1,
|
||||
activity_id: activity.id,
|
||||
unreachable_since: true
|
||||
})
|
||||
|> Publisher.publish_one()
|
||||
|
||||
assert {:ok, %{body: "port 80"}} =
|
||||
Publisher.publish_one(%{
|
||||
Publisher.prepare_one(%{
|
||||
inbox: inbox80,
|
||||
json: "{}",
|
||||
actor: actor,
|
||||
id: 1,
|
||||
activity_id: activity.id,
|
||||
unreachable_since: true
|
||||
})
|
||||
end
|
||||
|
||||
test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is not specified",
|
||||
Instances,
|
||||
[:passthrough],
|
||||
[] do
|
||||
actor = insert(:user)
|
||||
inbox = "http://200.site/users/nick1/inbox"
|
||||
|
||||
assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
|
||||
assert called(Instances.set_reachable(inbox))
|
||||
end
|
||||
|
||||
test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is set",
|
||||
Instances,
|
||||
[:passthrough],
|
||||
[] do
|
||||
actor = insert(:user)
|
||||
inbox = "http://200.site/users/nick1/inbox"
|
||||
|
||||
assert {:ok, _} =
|
||||
Publisher.publish_one(%{
|
||||
inbox: inbox,
|
||||
json: "{}",
|
||||
actor: actor,
|
||||
id: 1,
|
||||
unreachable_since: NaiveDateTime.utc_now()
|
||||
})
|
||||
|
||||
assert called(Instances.set_reachable(inbox))
|
||||
end
|
||||
|
||||
test_with_mock "does NOT call `Instances.set_reachable` on successful federation if `unreachable_since` is nil",
|
||||
Instances,
|
||||
[:passthrough],
|
||||
[] do
|
||||
actor = insert(:user)
|
||||
inbox = "http://200.site/users/nick1/inbox"
|
||||
|
||||
assert {:ok, _} =
|
||||
Publisher.publish_one(%{
|
||||
inbox: inbox,
|
||||
json: "{}",
|
||||
actor: actor,
|
||||
id: 1,
|
||||
unreachable_since: nil
|
||||
})
|
||||
|
||||
refute called(Instances.set_reachable(inbox))
|
||||
end
|
||||
|
||||
@tag capture_log: true
|
||||
test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code",
|
||||
Instances,
|
||||
[:passthrough],
|
||||
[] do
|
||||
actor = insert(:user)
|
||||
inbox = "http://404.site/users/nick1/inbox"
|
||||
|
||||
assert {:discard, _} =
|
||||
Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
|
||||
|
||||
assert called(Instances.set_unreachable(inbox))
|
||||
end
|
||||
|
||||
test_with_mock "it calls `Instances.set_unreachable` on target inbox on request error of any kind",
|
||||
Instances,
|
||||
[:passthrough],
|
||||
[] do
|
||||
actor = insert(:user)
|
||||
inbox = "http://connrefused.site/users/nick1/inbox"
|
||||
|
||||
assert capture_log(fn ->
|
||||
assert {:error, _} =
|
||||
Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
|
||||
end) =~ "connrefused"
|
||||
|
||||
assert called(Instances.set_unreachable(inbox))
|
||||
end
|
||||
|
||||
test_with_mock "does NOT call `Instances.set_unreachable` if target is reachable",
|
||||
Instances,
|
||||
[:passthrough],
|
||||
[] do
|
||||
actor = insert(:user)
|
||||
inbox = "http://200.site/users/nick1/inbox"
|
||||
|
||||
assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
|
||||
|
||||
refute called(Instances.set_unreachable(inbox))
|
||||
end
|
||||
|
||||
test_with_mock "does NOT call `Instances.set_unreachable` if target instance has non-nil `unreachable_since`",
|
||||
Instances,
|
||||
[:passthrough],
|
||||
[] do
|
||||
actor = insert(:user)
|
||||
inbox = "http://connrefused.site/users/nick1/inbox"
|
||||
|
||||
assert capture_log(fn ->
|
||||
assert {:error, _} =
|
||||
Publisher.publish_one(%{
|
||||
inbox: inbox,
|
||||
json: "{}",
|
||||
actor: actor,
|
||||
id: 1,
|
||||
unreachable_since: NaiveDateTime.utc_now()
|
||||
})
|
||||
end) =~ "connrefused"
|
||||
|
||||
refute called(Instances.set_unreachable(inbox))
|
||||
|> Publisher.publish_one()
|
||||
end
|
||||
end
|
||||
|
||||
describe "publish/2" do
|
||||
test_with_mock "doesn't publish a non-public activity to quarantined instances.",
|
||||
Pleroma.Web.ActivityPub.Publisher,
|
||||
[:passthrough],
|
||||
[] do
|
||||
test "doesn't publish a non-public activity to quarantined instances." do
|
||||
Config.put([:instance, :quarantined_instances], [{"domain.com", "some reason"}])
|
||||
|
||||
follower =
|
||||
|
|
@ -307,19 +193,18 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
|
|||
|
||||
assert res == :ok
|
||||
|
||||
assert not called(
|
||||
Publisher.enqueue_one(%{
|
||||
inbox: "https://domain.com/users/nick1/inbox",
|
||||
actor_id: actor.id,
|
||||
id: note_activity.data["id"]
|
||||
})
|
||||
)
|
||||
refute_enqueued(
|
||||
worker: "Pleroma.Workers.PublisherWorker",
|
||||
args: %{
|
||||
"params" => %{
|
||||
inbox: "https://domain.com/users/nick1/inbox",
|
||||
activity_id: note_activity.id
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
test_with_mock "Publishes a non-public activity to non-quarantined instances.",
|
||||
Pleroma.Web.ActivityPub.Publisher,
|
||||
[:passthrough],
|
||||
[] do
|
||||
test "Publishes a non-public activity to non-quarantined instances." do
|
||||
Config.put([:instance, :quarantined_instances], [{"somedomain.com", "some reason"}])
|
||||
|
||||
follower =
|
||||
|
|
@ -343,22 +228,19 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
|
|||
|
||||
assert res == :ok
|
||||
|
||||
assert called(
|
||||
Publisher.enqueue_one(
|
||||
%{
|
||||
inbox: "https://domain.com/users/nick1/inbox",
|
||||
actor_id: actor.id,
|
||||
id: note_activity.data["id"]
|
||||
},
|
||||
priority: 1
|
||||
)
|
||||
)
|
||||
assert_enqueued(
|
||||
worker: "Pleroma.Workers.PublisherWorker",
|
||||
args: %{
|
||||
"params" => %{
|
||||
inbox: "https://domain.com/users/nick1/inbox",
|
||||
activity_id: note_activity.id
|
||||
}
|
||||
},
|
||||
priority: 1
|
||||
)
|
||||
end
|
||||
|
||||
test_with_mock "Publishes to directly addressed actors with higher priority.",
|
||||
Pleroma.Web.ActivityPub.Publisher,
|
||||
[:passthrough],
|
||||
[] do
|
||||
test "Publishes to directly addressed actors with higher priority." do
|
||||
note_activity = insert(:direct_note_activity)
|
||||
|
||||
actor = Pleroma.User.get_by_ap_id(note_activity.data["actor"])
|
||||
|
|
@ -367,22 +249,58 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
|
|||
|
||||
assert res == :ok
|
||||
|
||||
assert called(
|
||||
Publisher.enqueue_one(
|
||||
%{
|
||||
inbox: :_,
|
||||
actor_id: actor.id,
|
||||
id: note_activity.data["id"]
|
||||
},
|
||||
priority: 0
|
||||
)
|
||||
)
|
||||
assert_enqueued(
|
||||
worker: "Pleroma.Workers.PublisherWorker",
|
||||
args: %{
|
||||
"op" => "publish_one",
|
||||
"params" => %{"activity_id" => note_activity.id}
|
||||
},
|
||||
priority: 0
|
||||
)
|
||||
end
|
||||
|
||||
test_with_mock "publishes an activity with BCC to all relevant peers.",
|
||||
Pleroma.Web.ActivityPub.Publisher,
|
||||
[:passthrough],
|
||||
[] do
|
||||
test "Publishes with the new actor if prepare_activity changes the actor." do
|
||||
mock(fn
|
||||
%{method: :post, url: "https://domain.com/users/nick1/inbox", body: body} ->
|
||||
{:ok, %Tesla.Env{status: 200, body: body}}
|
||||
end)
|
||||
|
||||
other_user =
|
||||
insert(:user, %{
|
||||
local: false,
|
||||
inbox: "https://domain.com/users/nick1/inbox"
|
||||
})
|
||||
|
||||
actor = insert(:user)
|
||||
replaced_actor = insert(:user)
|
||||
|
||||
note_activity =
|
||||
insert(:note_activity,
|
||||
user: actor,
|
||||
data_attrs: %{"to" => [other_user.ap_id]}
|
||||
)
|
||||
|
||||
Pleroma.Web.ActivityPub.TransmogrifierMock
|
||||
|> Mox.expect(:prepare_activity, fn data ->
|
||||
{:ok, Map.put(data, "actor", replaced_actor.ap_id)}
|
||||
end)
|
||||
|
||||
prepared =
|
||||
Publisher.prepare_one(%{
|
||||
inbox: "https://domain.com/users/nick1/inbox",
|
||||
activity_id: note_activity.id,
|
||||
cc: ["https://domain.com/users/nick2/inbox"]
|
||||
})
|
||||
|
||||
{:ok, decoded} = Jason.decode(prepared.json)
|
||||
assert decoded["actor"] == replaced_actor.ap_id
|
||||
|
||||
{:ok, published} = Publisher.publish_one(prepared)
|
||||
sent_activity = Jason.decode!(published.body)
|
||||
assert sent_activity["actor"] == replaced_actor.ap_id
|
||||
end
|
||||
|
||||
test "publishes an activity with BCC to all relevant peers." do
|
||||
follower =
|
||||
insert(:user, %{
|
||||
local: false,
|
||||
|
|
@ -403,19 +321,45 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
|
|||
res = Publisher.publish(actor, note_activity)
|
||||
assert res == :ok
|
||||
|
||||
assert called(
|
||||
Publisher.enqueue_one(%{
|
||||
inbox: "https://domain.com/users/nick1/inbox",
|
||||
actor_id: actor.id,
|
||||
id: note_activity.data["id"]
|
||||
})
|
||||
)
|
||||
assert_enqueued(
|
||||
worker: "Pleroma.Workers.PublisherWorker",
|
||||
args: %{
|
||||
"params" => %{
|
||||
inbox: "https://domain.com/users/nick1/inbox",
|
||||
activity_id: note_activity.id
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
test_with_mock "publishes a delete activity to peers who signed fetch requests to the create acitvity/object.",
|
||||
Pleroma.Web.ActivityPub.Publisher,
|
||||
[:passthrough],
|
||||
[] do
|
||||
test "activity with BCC is published to a list member." do
|
||||
actor = insert(:user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "list"}, actor)
|
||||
list_member = insert(:user, %{local: false})
|
||||
|
||||
Pleroma.List.follow(list, list_member)
|
||||
|
||||
note_activity =
|
||||
insert(:note_activity,
|
||||
# recipients: [follower.ap_id],
|
||||
data_attrs: %{"bcc" => [list.ap_id]}
|
||||
)
|
||||
|
||||
res = Publisher.publish(actor, note_activity)
|
||||
assert res == :ok
|
||||
|
||||
assert_enqueued(
|
||||
worker: "Pleroma.Workers.PublisherWorker",
|
||||
args: %{
|
||||
"params" => %{
|
||||
inbox: list_member.inbox,
|
||||
activity_id: note_activity.id
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
test "publishes a delete activity to peers who signed fetch requests to the create acitvity/object." do
|
||||
fetcher =
|
||||
insert(:user,
|
||||
local: false,
|
||||
|
|
@ -453,27 +397,209 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
|
|||
res = Publisher.publish(actor, delete)
|
||||
assert res == :ok
|
||||
|
||||
assert called(
|
||||
Publisher.enqueue_one(
|
||||
%{
|
||||
inbox: "https://domain.com/users/nick1/inbox",
|
||||
actor_id: actor.id,
|
||||
id: delete.data["id"]
|
||||
},
|
||||
priority: 1
|
||||
)
|
||||
)
|
||||
assert_enqueued(
|
||||
worker: "Pleroma.Workers.PublisherWorker",
|
||||
args: %{
|
||||
"params" => %{
|
||||
inbox: "https://domain.com/users/nick1/inbox",
|
||||
activity_id: delete.id
|
||||
}
|
||||
},
|
||||
priority: 1
|
||||
)
|
||||
|
||||
assert called(
|
||||
Publisher.enqueue_one(
|
||||
%{
|
||||
inbox: "https://domain2.com/users/nick1/inbox",
|
||||
actor_id: actor.id,
|
||||
id: delete.data["id"]
|
||||
},
|
||||
priority: 1
|
||||
)
|
||||
)
|
||||
assert_enqueued(
|
||||
worker: "Pleroma.Workers.PublisherWorker",
|
||||
args: %{
|
||||
"params" => %{
|
||||
inbox: "https://domain2.com/users/nick1/inbox",
|
||||
activity_id: delete.id
|
||||
}
|
||||
},
|
||||
priority: 1
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
test "cc in prepared json for a follow request is an empty list" do
|
||||
user = insert(:user)
|
||||
remote_user = insert(:user, local: false)
|
||||
|
||||
{:ok, _, _, activity} = CommonAPI.follow(remote_user, user)
|
||||
|
||||
assert_enqueued(
|
||||
worker: "Pleroma.Workers.PublisherWorker",
|
||||
args: %{
|
||||
"activity_id" => activity.id,
|
||||
"op" => "publish"
|
||||
}
|
||||
)
|
||||
|
||||
ObanHelpers.perform_all()
|
||||
|
||||
expected_params =
|
||||
%{
|
||||
"activity_id" => activity.id,
|
||||
"inbox" => remote_user.inbox,
|
||||
"unreachable_since" => nil
|
||||
}
|
||||
|
||||
assert_enqueued(
|
||||
worker: "Pleroma.Workers.PublisherWorker",
|
||||
args: %{
|
||||
"op" => "publish_one",
|
||||
"params" => expected_params
|
||||
}
|
||||
)
|
||||
|
||||
# params need to be atom keys for Publisher.prepare_one.
|
||||
# this is done in the Oban job.
|
||||
expected_params = Map.new(expected_params, fn {k, v} -> {String.to_atom(k), v} end)
|
||||
|
||||
%{json: json} = Publisher.prepare_one(expected_params)
|
||||
|
||||
{:ok, decoded} = Jason.decode(json)
|
||||
|
||||
assert decoded["cc"] == []
|
||||
end
|
||||
|
||||
test "unlisted activities retain public address in cc" do
|
||||
user = insert(:user)
|
||||
|
||||
# simulate unlistd activity by only having
|
||||
# public address in cc
|
||||
activity =
|
||||
insert(:note_activity,
|
||||
user: user,
|
||||
data_attrs: %{
|
||||
"cc" => [@as_public],
|
||||
"to" => [user.follower_address]
|
||||
}
|
||||
)
|
||||
|
||||
assert @as_public in activity.data["cc"]
|
||||
|
||||
prepared =
|
||||
Publisher.prepare_one(%{
|
||||
inbox: "https://remote.instance/users/someone/inbox",
|
||||
activity_id: activity.id
|
||||
})
|
||||
|
||||
{:ok, decoded} = Jason.decode(prepared.json)
|
||||
|
||||
assert @as_public in decoded["cc"]
|
||||
|
||||
# maybe we also have another inbox in cc
|
||||
# during Publishing
|
||||
activity =
|
||||
insert(:note_activity,
|
||||
user: user,
|
||||
data_attrs: %{
|
||||
"cc" => [@as_public],
|
||||
"to" => [user.follower_address]
|
||||
}
|
||||
)
|
||||
|
||||
prepared =
|
||||
Publisher.prepare_one(%{
|
||||
inbox: "https://remote.instance/users/someone/inbox",
|
||||
activity_id: activity.id,
|
||||
cc: ["https://remote.instance/users/someone_else/inbox"]
|
||||
})
|
||||
|
||||
{:ok, decoded} = Jason.decode(prepared.json)
|
||||
|
||||
assert decoded["cc"] == [@as_public, "https://remote.instance/users/someone_else/inbox"]
|
||||
end
|
||||
|
||||
test "public address in cc parameter is preserved" do
|
||||
user = insert(:user)
|
||||
|
||||
cc_with_public = [@as_public, "https://example.org/users/other"]
|
||||
|
||||
activity =
|
||||
insert(:note_activity,
|
||||
user: user,
|
||||
data_attrs: %{
|
||||
"cc" => cc_with_public,
|
||||
"to" => [user.follower_address]
|
||||
}
|
||||
)
|
||||
|
||||
assert @as_public in activity.data["cc"]
|
||||
|
||||
prepared =
|
||||
Publisher.prepare_one(%{
|
||||
inbox: "https://remote.instance/users/someone/inbox",
|
||||
activity_id: activity.id,
|
||||
cc: cc_with_public
|
||||
})
|
||||
|
||||
{:ok, decoded} = Jason.decode(prepared.json)
|
||||
|
||||
assert cc_with_public == decoded["cc"]
|
||||
end
|
||||
|
||||
test "cc parameter is preserved" do
|
||||
user = insert(:user)
|
||||
|
||||
activity =
|
||||
insert(:note_activity,
|
||||
user: user,
|
||||
data_attrs: %{
|
||||
"cc" => ["https://example.com/specific/user"],
|
||||
"to" => [user.follower_address]
|
||||
}
|
||||
)
|
||||
|
||||
prepared =
|
||||
Publisher.prepare_one(%{
|
||||
inbox: "https://remote.instance/users/someone/inbox",
|
||||
activity_id: activity.id,
|
||||
cc: ["https://example.com/specific/user"]
|
||||
})
|
||||
|
||||
{:ok, decoded} = Jason.decode(prepared.json)
|
||||
|
||||
assert decoded["cc"] == ["https://example.com/specific/user"]
|
||||
end
|
||||
|
||||
describe "prepare_one/1 with reporter anonymization" do
|
||||
test "signs with the anonymized actor keys when Transmogrifier changes actor" do
|
||||
Pleroma.SignatureMock
|
||||
|> Mox.stub(:signed_date, fn -> Pleroma.Signature.signed_date() end)
|
||||
|> Mox.expect(:sign, fn %Pleroma.User{} = user, _headers ->
|
||||
send(self(), {:signed_as, user.ap_id})
|
||||
"TESTSIG"
|
||||
end)
|
||||
|
||||
placeholder = insert(:user)
|
||||
reporter = insert(:user)
|
||||
target_account = insert(:user)
|
||||
|
||||
clear_config([:activitypub, :anonymize_reporter], true)
|
||||
clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname)
|
||||
|
||||
{:ok, reported} = CommonAPI.post(target_account, %{status: "content"})
|
||||
context = Utils.generate_context_id()
|
||||
|
||||
{:ok, activity} =
|
||||
ActivityPub.flag(%{
|
||||
actor: reporter,
|
||||
context: context,
|
||||
account: target_account,
|
||||
statuses: [reported],
|
||||
content: "reason"
|
||||
})
|
||||
|
||||
_prepared =
|
||||
Publisher.prepare_one(%{
|
||||
inbox: "http://remote.example/users/alice/inbox",
|
||||
activity_id: activity.id
|
||||
})
|
||||
|
||||
assert_received {:signed_as, ap_id}
|
||||
assert ap_id == placeholder.ap_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do
|
|||
test "returns activity" do
|
||||
user = insert(:user)
|
||||
service_actor = Relay.get_actor()
|
||||
CommonAPI.follow(service_actor, user)
|
||||
CommonAPI.follow(user, service_actor)
|
||||
assert "#{user.ap_id}/followers" in User.following(service_actor)
|
||||
assert {:ok, %Activity{} = activity} = Relay.unfollow(user.ap_id)
|
||||
assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay"
|
||||
|
|
@ -74,7 +74,7 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do
|
|||
end)
|
||||
|
||||
service_actor = Relay.get_actor()
|
||||
CommonAPI.follow(service_actor, user)
|
||||
CommonAPI.follow(user, service_actor)
|
||||
assert "#{user.ap_id}/followers" in User.following(service_actor)
|
||||
|
||||
assert Pleroma.Repo.get_by(
|
||||
|
|
@ -113,7 +113,6 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do
|
|||
assert Relay.publish(activity) == {:error, "Not implemented"}
|
||||
end
|
||||
|
||||
@tag capture_log: true
|
||||
test "returns error when activity not public" do
|
||||
activity = insert(:direct_note_activity)
|
||||
assert Relay.publish(activity) == {:error, false}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects.DeleteTest do
|
|||
|
||||
{:ok, op} = CommonAPI.post(other_user, %{status: "big oof"})
|
||||
{:ok, post} = CommonAPI.post(user, %{status: "hey", in_reply_to_id: op})
|
||||
{:ok, favorite} = CommonAPI.favorite(user, post.id)
|
||||
{:ok, favorite} = CommonAPI.favorite(post.id, user)
|
||||
object = Object.normalize(post, fetch: false)
|
||||
{:ok, delete_data, _meta} = Builder.delete(user, object.data["id"])
|
||||
{:ok, delete, _meta} = ActivityPub.persist(delete_data, local: true)
|
||||
|
|
|
|||
|
|
@ -54,20 +54,17 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
|
|||
[
|
||||
stream: fn _, _ -> nil end
|
||||
]
|
||||
},
|
||||
{
|
||||
Pleroma.Web.Push,
|
||||
[],
|
||||
[
|
||||
send: fn _ -> nil end
|
||||
]
|
||||
}
|
||||
]) do
|
||||
SideEffects.handle_after_transaction(meta)
|
||||
|
||||
assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification))
|
||||
assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_))
|
||||
assert called(Pleroma.Web.Push.send(notification))
|
||||
|
||||
assert_enqueued(
|
||||
worker: "Pleroma.Workers.WebPusherWorker",
|
||||
args: %{"notification_id" => notification.id, "op" => "web_push"}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -516,10 +513,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
|
|||
poster = insert(:user)
|
||||
user = insert(:user)
|
||||
{:ok, post} = CommonAPI.post(poster, %{status: "hey"})
|
||||
{:ok, like} = CommonAPI.favorite(user, post.id)
|
||||
{:ok, like} = CommonAPI.favorite(post.id, user)
|
||||
{:ok, reaction} = CommonAPI.react_with_emoji(post.id, user, "👍")
|
||||
{:ok, announce} = CommonAPI.repeat(post.id, user)
|
||||
{:ok, block} = CommonAPI.block(user, poster)
|
||||
{:ok, block} = CommonAPI.block(poster, user)
|
||||
|
||||
{:ok, undo_data, _meta} = Builder.undo(user, like)
|
||||
{:ok, like_undo, _meta} = ActivityPub.persist(undo_data, local: true)
|
||||
|
|
@ -787,13 +784,15 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
|
|||
{:ok, post} = CommonAPI.post(poster, %{status: "hey"})
|
||||
{:ok, private_post} = CommonAPI.post(poster, %{status: "hey", visibility: "private"})
|
||||
|
||||
{:ok, announce_data, _meta} = Builder.announce(user, post.object, public: true)
|
||||
{:ok, announce_data, _meta} = Builder.announce(user, post.object, visibility: "public")
|
||||
|
||||
{:ok, private_announce_data, _meta} =
|
||||
Builder.announce(user, private_post.object, public: false)
|
||||
Builder.announce(user, private_post.object, visibility: "private")
|
||||
|
||||
{:ok, relay_announce_data, _meta} =
|
||||
Builder.announce(Pleroma.Web.ActivityPub.Relay.get_actor(), post.object, public: true)
|
||||
Builder.announce(Pleroma.Web.ActivityPub.Relay.get_actor(), post.object,
|
||||
visibility: "public"
|
||||
)
|
||||
|
||||
{:ok, announce, _meta} = ActivityPub.persist(announce_data, local: true)
|
||||
{:ok, private_announce, _meta} = ActivityPub.persist(private_announce_data, local: true)
|
||||
|
|
@ -834,7 +833,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
|
|||
user = insert(:user)
|
||||
followed = insert(:user)
|
||||
|
||||
{:ok, _, _, follow_activity} = CommonAPI.follow(user, followed)
|
||||
{:ok, _, _, follow_activity} = CommonAPI.follow(followed, user)
|
||||
|
||||
{:ok, reject_data, []} = Builder.reject(followed, follow_activity)
|
||||
{:ok, reject, _meta} = ActivityPub.persist(reject_data, local: true)
|
||||
|
|
@ -965,7 +964,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
|
|||
group: group,
|
||||
poster: poster
|
||||
} do
|
||||
{:ok, _} = CommonAPI.block(group, poster)
|
||||
{:ok, _} = CommonAPI.block(poster, group)
|
||||
create_activity_data = make_create.([group])
|
||||
{:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.AcceptHandlingTest do
|
|||
{:ok, follower, followed} = User.follow(follower, followed)
|
||||
assert User.following?(follower, followed) == true
|
||||
|
||||
{:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed)
|
||||
{:ok, _, _, follow_activity} = CommonAPI.follow(followed, follower)
|
||||
|
||||
accept_data =
|
||||
File.read!("test/fixtures/mastodon-accept-activity.json")
|
||||
|
|
@ -48,7 +48,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.AcceptHandlingTest do
|
|||
follower = insert(:user)
|
||||
followed = insert(:user, is_locked: true)
|
||||
|
||||
{:ok, _, _, follow_activity} = CommonAPI.follow(follower, followed)
|
||||
{:ok, _, _, follow_activity} = CommonAPI.follow(followed, follower)
|
||||
|
||||
accept_data =
|
||||
File.read!("test/fixtures/mastodon-accept-activity.json")
|
||||
|
|
|
|||
|
|
@ -83,7 +83,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnnounceHandlingTest do
|
|||
assert(Activity.get_create_by_object_ap_id(data["object"]))
|
||||
end
|
||||
|
||||
@tag capture_log: true
|
||||
test "it works for incoming announces with an existing activity" do
|
||||
user = insert(:user)
|
||||
{:ok, activity} = CommonAPI.post(user, %{status: "hey"})
|
||||
|
|
@ -136,7 +135,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnnounceHandlingTest do
|
|||
assert object.data["content"] == "this is a private toot"
|
||||
end
|
||||
|
||||
@tag capture_log: true
|
||||
test "it rejects incoming announces with an inlined activity from another origin" do
|
||||
Tesla.Mock.mock(fn
|
||||
%{method: :get} -> %Tesla.Env{status: 404, body: ""}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnswerHandlingTest do
|
|||
|> Kernel.put_in(["object", "to"], user.ap_id)
|
||||
|
||||
{:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
|
||||
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
|
||||
{:ok, data} = Transmogrifier.prepare_activity(activity.data)
|
||||
|
||||
assert data["object"]["type"] == "Note"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -86,7 +86,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.DeleteHandlingTest do
|
|||
assert match?({:error, _}, Transmogrifier.handle_incoming(data))
|
||||
end
|
||||
|
||||
@tag capture_log: true
|
||||
test "it works for incoming user deletes" do
|
||||
%{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin")
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiTagBuildingTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
test "it encodes the id to be a valid url" do
|
||||
name = "hanapog"
|
||||
url = "https://misskey.local.live/emojis/hana pog.png"
|
||||
|
||||
tag = Pleroma.Emoji.build_emoji_tag({name, url})
|
||||
|
||||
assert tag["id"] == "https://misskey.local.live/emojis/hana%20pog.png"
|
||||
end
|
||||
|
||||
test "it does not double-encode already encoded urls" do
|
||||
name = "hanapog"
|
||||
url = "https://misskey.local.live/emojis/hana%20pog.png"
|
||||
|
||||
tag = Pleroma.Emoji.build_emoji_tag({name, url})
|
||||
|
||||
assert tag["id"] == url
|
||||
end
|
||||
|
||||
test "it encodes disallowed path characters" do
|
||||
name = "hanapog"
|
||||
url = "https://example.com/emojis/hana[pog].png"
|
||||
|
||||
tag = Pleroma.Emoji.build_emoji_tag({name, url})
|
||||
|
||||
assert tag["id"] == "https://example.com/emojis/hana%5Bpog%5D.png"
|
||||
end
|
||||
|
||||
test "local_url does not decode percent in filenames" do
|
||||
url = Pleroma.Emoji.local_url("/emoji/hana%20pog.png")
|
||||
|
||||
assert url == Pleroma.Web.Endpoint.url() <> "/emoji/hana%2520pog.png"
|
||||
|
||||
tag = Pleroma.Emoji.build_emoji_tag({"hanapog", url})
|
||||
|
||||
assert tag["id"] == url
|
||||
end
|
||||
|
||||
test "local_url encodes question marks in filenames" do
|
||||
url = Pleroma.Emoji.local_url("/emoji/file?name.png")
|
||||
|
||||
assert url == Pleroma.Web.Endpoint.url() <> "/emoji/file%3Fname.png"
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
# 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.Transmogrifier.FedidevFunAttachmentsTest do
|
||||
use Oban.Testing, repo: Pleroma.Repo
|
||||
use Pleroma.DataCase
|
||||
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
setup_all do
|
||||
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||
:ok
|
||||
end
|
||||
|
||||
defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do
|
||||
case User.get_cached_by_ap_id(ap_id) do
|
||||
%User{} ->
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
insert(:user,
|
||||
local: false,
|
||||
ap_id: ap_id,
|
||||
follower_address: ap_id <> "/followers",
|
||||
following_address: ap_id <> "/following",
|
||||
featured_address: ap_id <> "/collections/featured",
|
||||
inbox: ap_id <> "/inbox"
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp ingest_data!(data) do
|
||||
ensure_remote_actor!(data["actor"])
|
||||
|
||||
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
|
||||
object = Object.normalize(activity.data["object"], fetch: false)
|
||||
|
||||
{activity, object}
|
||||
end
|
||||
|
||||
defp ingest_fixture!(rel_path) do
|
||||
rel_path
|
||||
|> File.read!()
|
||||
|> Jason.decode!()
|
||||
|> ingest_data!()
|
||||
end
|
||||
|
||||
test "drops non-http(s) attachment href" do
|
||||
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/attachment_payto_link.json")
|
||||
assert object.data["attachment"] == []
|
||||
end
|
||||
|
||||
test "drops embedded non-attachment objects" do
|
||||
{_activity, object} =
|
||||
ingest_fixture!("test/fixtures/fedidev.fun/attachment_embedded_note.json")
|
||||
|
||||
assert object.data["attachment"] == []
|
||||
end
|
||||
|
||||
test "accepts attachment url as list and keeps the first link" do
|
||||
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/attachment_url_list.json")
|
||||
|
||||
[attachment] = object.data["attachment"]
|
||||
[%{"href" => href} | _] = attachment["url"]
|
||||
|
||||
assert href == "https://fedidev.fun/images/007.png"
|
||||
end
|
||||
|
||||
test "drops malformed attachment list entries" do
|
||||
data = File.read!("test/fixtures/fedidev.fun/attachment_url_list.json") |> Jason.decode!()
|
||||
data = update_in(data, ["object", "attachment"], &["malformed" | List.wrap(&1)])
|
||||
|
||||
{_activity, object} = ingest_data!(data)
|
||||
|
||||
assert length(object.data["attachment"]) == 1
|
||||
end
|
||||
|
||||
test "accepts a URI string in an attachment url list" do
|
||||
data = File.read!("test/fixtures/fedidev.fun/attachment_url_list.json") |> Jason.decode!()
|
||||
|
||||
data =
|
||||
put_in(data, ["object", "attachment", "url"], [
|
||||
"https://fedidev.fun/images/007.png"
|
||||
])
|
||||
|
||||
{_activity, object} = ingest_data!(data)
|
||||
|
||||
assert [%{"url" => [%{"href" => "https://fedidev.fun/images/007.png"}]}] =
|
||||
object.data["attachment"]
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
# 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.Transmogrifier.FedidevFunEmojiTest do
|
||||
use Oban.Testing, repo: Pleroma.Repo
|
||||
use Pleroma.DataCase
|
||||
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
setup_all do
|
||||
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||
:ok
|
||||
end
|
||||
|
||||
defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do
|
||||
case User.get_cached_by_ap_id(ap_id) do
|
||||
%User{} ->
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
insert(:user,
|
||||
local: false,
|
||||
ap_id: ap_id,
|
||||
follower_address: ap_id <> "/followers",
|
||||
following_address: ap_id <> "/following",
|
||||
featured_address: ap_id <> "/collections/featured",
|
||||
inbox: ap_id <> "/inbox"
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp ingest_data!(data) do
|
||||
ensure_remote_actor!(data["actor"])
|
||||
|
||||
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
|
||||
object = Object.normalize(activity.data["object"], fetch: false)
|
||||
|
||||
{activity, object}
|
||||
end
|
||||
|
||||
defp ingest_fixture!(rel_path) do
|
||||
rel_path
|
||||
|> File.read!()
|
||||
|> Jason.decode!()
|
||||
|> ingest_data!()
|
||||
end
|
||||
|
||||
test "ingests Emoji tag with string icon" do
|
||||
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/emoji_icon_string.json")
|
||||
assert object.data["emoji"]["cow"] == "https://fedidev.fun/static/cow.png"
|
||||
end
|
||||
|
||||
test "ingests Emoji tag with icon missing type" do
|
||||
{_activity, object} =
|
||||
ingest_fixture!("test/fixtures/fedidev.fun/emoji_icon_url_missing_type.json")
|
||||
|
||||
assert object.data["emoji"]["cow2"] == "https://fedidev.fun/static/cow.png"
|
||||
end
|
||||
|
||||
test "ignores Emoji tag missing icon" do
|
||||
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/emoji_missing_icon.json")
|
||||
assert object.data["emoji"] == %{}
|
||||
end
|
||||
|
||||
test "ignores Emoji tag missing a string name" do
|
||||
data = File.read!("test/fixtures/fedidev.fun/emoji_icon_string.json") |> Jason.decode!()
|
||||
data = put_in(data, ["object", "tag", Access.at(0), "name"], nil)
|
||||
|
||||
{_activity, object} = ingest_data!(data)
|
||||
|
||||
assert object.data["emoji"] == %{}
|
||||
end
|
||||
|
||||
test "ignores Emoji tag with a non-http icon" do
|
||||
data = File.read!("test/fixtures/fedidev.fun/emoji_icon_string.json") |> Jason.decode!()
|
||||
data = put_in(data, ["object", "tag", Access.at(0), "icon"], "file:///tmp/cow.png")
|
||||
|
||||
{_activity, object} = ingest_data!(data)
|
||||
|
||||
assert object.data["emoji"] == %{}
|
||||
end
|
||||
|
||||
test "normalizes a list-typed Emoji tag before building the emoji map" do
|
||||
data = File.read!("test/fixtures/fedidev.fun/emoji_icon_string.json") |> Jason.decode!()
|
||||
|
||||
data =
|
||||
put_in(data, ["object", "tag", Access.at(0), "type"], [
|
||||
"https://example.com/ns#CustomTag",
|
||||
"Emoji"
|
||||
])
|
||||
|
||||
{_activity, object} = ingest_data!(data)
|
||||
|
||||
assert object.data["emoji"]["cow"] == "https://fedidev.fun/static/cow.png"
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue