Merge branch 'develop' into webhooks
This commit is contained in:
commit
83dcc4ae36
59 changed files with 2245 additions and 148 deletions
|
|
@ -144,7 +144,13 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
|
|||
quarantined_instances: [],
|
||||
managed_config: true,
|
||||
static_dir: "instance/static/",
|
||||
allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"],
|
||||
allowed_post_formats: [
|
||||
"text/plain",
|
||||
"text/html",
|
||||
"text/markdown",
|
||||
"text/bbcode",
|
||||
"text/x.misskeymarkdown"
|
||||
],
|
||||
autofollowed_nicknames: [],
|
||||
max_pinned_statuses: 1,
|
||||
attachment_links: 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
|
||||
|
|
@ -279,6 +279,26 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|
|||
end)
|
||||
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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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") ==
|
||||
|
|
|
|||
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
|
||||
|
|
@ -950,6 +950,50 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|
|||
refute Activity.get_by_ap_id(data["id"])
|
||||
end
|
||||
|
||||
test "does not process post with Host header not for us", %{conn: conn} do
|
||||
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
|
||||
object_id = "https://one.com/objects/inbox-forged-note"
|
||||
|
||||
data = %{
|
||||
"type" => "Create",
|
||||
"actor" => alice.ap_id,
|
||||
"id" => "https://one.com/activities/inbox-forged-create",
|
||||
"context" => "https://one.com/contexts/inbox-forged-create",
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => [],
|
||||
"object" => %{
|
||||
"type" => "Note",
|
||||
"id" => object_id,
|
||||
"actor" => alice.ap_id,
|
||||
"attributedTo" => alice.ap_id,
|
||||
"context" => "https://one.com/contexts/inbox-forged-create",
|
||||
"content" => "forged post",
|
||||
"published" => "2024-07-25T13:33:31Z",
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => []
|
||||
}
|
||||
}
|
||||
|
||||
# Plug will complain when replacing raw host header with put_req_header.
|
||||
# The Plug way is updating conn.host, but that isn't the raw header
|
||||
# and that isn't used in the EnsureHostMatchesPlug, because it doesn't include the port.
|
||||
conn =
|
||||
conn
|
||||
|> assign_valid_signature_for_actor(alice)
|
||||
|> delete_req_header("host")
|
||||
|> put_req_header("content-type", "application/activity+json")
|
||||
|
||||
conn = %{conn | req_headers: conn.req_headers ++ [{"host", "invalid.example.com"}]}
|
||||
conn = post(conn, "/inbox", data)
|
||||
|
||||
assert "Host header does not match this instance" == conn.resp_body
|
||||
assert 400 == conn.status
|
||||
assert true == conn.halted
|
||||
|
||||
refute Activity.get_by_ap_id(data["id"])
|
||||
refute Object.get_by_ap_id(object_id)
|
||||
end
|
||||
|
||||
test "accept follow activity", %{conn: conn} do
|
||||
clear_config([:instance, :federating], true)
|
||||
relay = Relay.get_actor()
|
||||
|
|
|
|||
|
|
@ -149,6 +149,171 @@ 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")
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,43 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
|
|||
assert activity.data["cc"] == [user.ap_id]
|
||||
end
|
||||
|
||||
test "it rejects Flag activities when both reporter and reported account are remote" do
|
||||
reporter = insert(:user, local: false, domain: "mastodon.cat")
|
||||
reported = insert(:user, local: false, domain: "nicecrew.digital")
|
||||
|
||||
message = %{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"actor" => reporter.ap_id,
|
||||
"content" => "blocked AND reported!!!",
|
||||
"object" => [reported.ap_id, "https://nicecrew.digital/objects/report-status"],
|
||||
"type" => "Flag"
|
||||
}
|
||||
|
||||
assert {:reject, reason} = Transmogrifier.handle_incoming(message)
|
||||
assert reason =~ "third-party report"
|
||||
refute "Flag" |> Pleroma.Activity.Queries.by_type() |> Pleroma.Repo.one()
|
||||
end
|
||||
|
||||
test "it accepts Flag activities with just actor id as object" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
||||
message = %{
|
||||
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||
"cc" => [user.ap_id],
|
||||
"object" => user.ap_id,
|
||||
"type" => "Flag",
|
||||
"content" => "blocked AND reported!!!",
|
||||
"actor" => other_user.ap_id
|
||||
}
|
||||
|
||||
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
|
||||
|
||||
assert activity.data["content"] == "blocked AND reported!!!"
|
||||
assert activity.data["actor"] == other_user.ap_id
|
||||
assert activity.data["cc"] == [user.ap_id]
|
||||
end
|
||||
|
||||
test "it accepts Move activities" do
|
||||
old_user = insert(:user)
|
||||
new_user = insert(:user)
|
||||
|
|
|
|||
|
|
@ -180,7 +180,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
|
|||
"https://www.w3.org/ns/activitystreams",
|
||||
"http://localhost:4001/schemas/litepub-0.1.jsonld",
|
||||
%{
|
||||
"@language" => "und"
|
||||
"@language" => "und",
|
||||
"htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -192,7 +193,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
|
|||
"https://www.w3.org/ns/activitystreams",
|
||||
"http://localhost:4001/schemas/litepub-0.1.jsonld",
|
||||
%{
|
||||
"@language" => "pl"
|
||||
"@language" => "pl",
|
||||
"htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -709,6 +709,47 @@ defmodule Pleroma.Web.CommonAPITest do
|
|||
assert object.data["source"]["content"] == post
|
||||
end
|
||||
|
||||
test "it renders MFM posts and marks their ActivityPub representation" do
|
||||
user = insert(:user)
|
||||
|
||||
post = "<p class='scrub-this'>$[spin.speed=1s 13:37]</p>"
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
status: post,
|
||||
content_type: "text/x.misskeymarkdown"
|
||||
})
|
||||
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
|
||||
assert object.data["htmlMfm"] == true
|
||||
|
||||
assert object.data["source"] == %{
|
||||
"content" => post,
|
||||
"mediaType" => "text/x.misskeymarkdown"
|
||||
}
|
||||
|
||||
assert object.data["content"] =~ ~s(class="mfm-spin")
|
||||
assert object.data["content"] =~ ~s(data-mfm-speed="1s")
|
||||
assert object.data["content"] =~ "13:37"
|
||||
refute object.data["content"] =~ "scrub-this"
|
||||
end
|
||||
|
||||
test "it falls back safely for malformed MFM" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
status: "$[spin malformed",
|
||||
content_type: "text/x.misskeymarkdown"
|
||||
})
|
||||
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
|
||||
refute object.data["content"] =~ ~s(class="mfm-spin")
|
||||
assert object.data["content"] =~ "malformed"
|
||||
end
|
||||
|
||||
test "it does not allow replies to direct messages that are not direct messages themselves" do
|
||||
user = insert(:user)
|
||||
|
||||
|
|
|
|||
|
|
@ -180,4 +180,179 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do
|
|||
|
||||
assert result[:pleroma][:non_anonymous] == true
|
||||
end
|
||||
|
||||
test "prefers votersCount over voters list when both are present" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
status: "Which flavor?",
|
||||
poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
|
||||
})
|
||||
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
|
||||
voter = insert(:user)
|
||||
{:ok, _, object} = CommonAPI.vote(object, voter, [0])
|
||||
|
||||
assert object.data["votersCount"] == 1
|
||||
assert length(object.data["voters"]) == 1
|
||||
|
||||
object = %{
|
||||
object
|
||||
| data: Map.put(object.data, "votersCount", 42)
|
||||
}
|
||||
|
||||
result = PollView.render("show.json", %{object: object})
|
||||
|
||||
assert result[:voters_count] == 42
|
||||
end
|
||||
|
||||
test "falls back to voters list when votersCount is absent" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
status: "Which flavor?",
|
||||
poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
|
||||
})
|
||||
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
|
||||
voter = insert(:user)
|
||||
{:ok, _, object} = CommonAPI.vote(object, voter, [0])
|
||||
|
||||
assert length(object.data["voters"]) == 1
|
||||
|
||||
data = Map.delete(object.data, "votersCount")
|
||||
object = %{object | data: data}
|
||||
|
||||
result = PollView.render("show.json", %{object: object})
|
||||
|
||||
assert result[:voters_count] == 1
|
||||
end
|
||||
|
||||
test "returns 0 when both votersCount and voters are absent" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
status: "Which flavor?",
|
||||
poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
|
||||
})
|
||||
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
|
||||
data =
|
||||
object.data
|
||||
|> Map.delete("votersCount")
|
||||
|> Map.delete("voters")
|
||||
|
||||
object = %{object | data: data}
|
||||
|
||||
result = PollView.render("show.json", %{object: object})
|
||||
|
||||
assert result[:voters_count] == 0
|
||||
end
|
||||
|
||||
test "returns 0 when voters list is empty" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
status: "Which flavor?",
|
||||
poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
|
||||
})
|
||||
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
|
||||
data =
|
||||
object.data
|
||||
|> Map.delete("votersCount")
|
||||
|> Map.put("voters", [])
|
||||
|
||||
object = %{object | data: data}
|
||||
|
||||
result = PollView.render("show.json", %{object: object})
|
||||
|
||||
assert result[:voters_count] == 0
|
||||
end
|
||||
|
||||
test "does not inflate votersCount when same voter picks multiple options" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
status: "Pick several",
|
||||
poll: %{options: ["a", "b", "c"], expires_in: 20, multiple: true}
|
||||
})
|
||||
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
|
||||
voter = insert(:user)
|
||||
{:ok, _, object} = CommonAPI.vote(object, voter, [0, 2])
|
||||
|
||||
assert object.data["votersCount"] == 1
|
||||
assert length(object.data["voters"]) == 1
|
||||
end
|
||||
|
||||
test "preserves votersCount from remote source when existing voter picks another option" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
status: "Pick several",
|
||||
poll: %{options: ["a", "b"], expires_in: 20, multiple: true}
|
||||
})
|
||||
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
|
||||
voter = insert(:user)
|
||||
{:ok, _, object} = CommonAPI.vote(object, voter, [0, 1])
|
||||
|
||||
object = %{object | data: Map.put(object.data, "votersCount", 14)}
|
||||
|
||||
result = PollView.render("show.json", %{object: object})
|
||||
|
||||
assert result[:voters_count] == 14
|
||||
end
|
||||
|
||||
test "returns 0 when votersCount is explicitly 0" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
status: "Pick one",
|
||||
poll: %{options: ["a", "b"], expires_in: 20}
|
||||
})
|
||||
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
|
||||
object = %{object | data: Map.put(object.data, "votersCount", 0)}
|
||||
|
||||
result = PollView.render("show.json", %{object: object})
|
||||
|
||||
assert result[:voters_count] == 0
|
||||
end
|
||||
|
||||
test "falls back to voters list when votersCount is nil" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user, %{
|
||||
status: "Pick one",
|
||||
poll: %{options: ["a", "b"], expires_in: 20}
|
||||
})
|
||||
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
|
||||
voter = insert(:user)
|
||||
{:ok, _, object} = CommonAPI.vote(object, voter, [0])
|
||||
|
||||
object = %{object | data: Map.put(object.data, "votersCount", nil)}
|
||||
|
||||
result = PollView.render("show.json", %{object: object})
|
||||
|
||||
assert result[:voters_count] == length(object.data["voters"])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
121
test/pleroma/web/plugs/ensure_host_matches_plug_test.exs
Normal file
121
test/pleroma/web/plugs/ensure_host_matches_plug_test.exs
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlugTest do
|
||||
use Pleroma.Web.ConnCase
|
||||
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.Plugs.EnsureHostMatchesPlug
|
||||
|
||||
import Plug.Conn
|
||||
import Tesla.Mock
|
||||
|
||||
setup do
|
||||
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||
:ok
|
||||
end
|
||||
|
||||
defp set_host(conn, host), do: %{conn | req_headers: conn.req_headers ++ [{"host", host}]}
|
||||
|
||||
describe "EnsureHostMatchesPlug" do
|
||||
setup do
|
||||
conn = build_conn(:post, "/cofe") |> assign(:valid_signature, true)
|
||||
[conn: conn]
|
||||
end
|
||||
|
||||
test "gracefully handles no Host header", %{conn: conn} do
|
||||
conn = EnsureHostMatchesPlug.call(conn, %{})
|
||||
|
||||
assert conn.status == 400
|
||||
assert conn.halted == true
|
||||
assert conn.resp_body == "Host header not provided"
|
||||
end
|
||||
|
||||
test "gracefully handles empty Host header", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> set_host("")
|
||||
|> EnsureHostMatchesPlug.call(%{})
|
||||
|
||||
assert conn.status == 400
|
||||
assert conn.halted == true
|
||||
assert conn.resp_body == "Host header not provided"
|
||||
end
|
||||
|
||||
test "it rejects Host header not matching Endpoint URL", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> set_host("invalid.example.com")
|
||||
|> EnsureHostMatchesPlug.call(%{})
|
||||
|
||||
assert conn.status == 400
|
||||
assert conn.halted == true
|
||||
assert conn.resp_body == "Host header does not match this instance"
|
||||
end
|
||||
|
||||
test "it rejects Host header not matching Endpoint with port", %{conn: conn} do
|
||||
endpoint = URI.parse(Endpoint.url())
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> set_host("invalid.example.com:#{endpoint.port}")
|
||||
|> EnsureHostMatchesPlug.call(%{})
|
||||
|
||||
assert conn.status == 400
|
||||
assert conn.halted == true
|
||||
assert conn.resp_body == "Host header does not match this instance"
|
||||
end
|
||||
|
||||
test "it rejects Host header not matching Endpoint port", %{conn: conn} do
|
||||
endpoint = URI.parse(Endpoint.url())
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> set_host("#{endpoint.host}:25")
|
||||
|> EnsureHostMatchesPlug.call(%{})
|
||||
|
||||
assert conn.status == 400
|
||||
assert conn.halted == true
|
||||
assert conn.resp_body == "Host header does not match this instance"
|
||||
end
|
||||
|
||||
test "it rejects multiple Host headers", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> set_host("host1.example.com")
|
||||
|> set_host("host2.example.com")
|
||||
|> EnsureHostMatchesPlug.call(%{})
|
||||
|
||||
assert conn.status == 400
|
||||
assert conn.halted == true
|
||||
assert conn.resp_body == "More than one Host header provided"
|
||||
end
|
||||
|
||||
test "it works for Host header without port", %{conn: conn} do
|
||||
endpoint = URI.parse(Endpoint.url())
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> set_host("#{endpoint.host}")
|
||||
|> EnsureHostMatchesPlug.call(%{})
|
||||
|
||||
assert conn.halted == false
|
||||
assert Map.get(conn.assigns, :valid_host_header, nil)
|
||||
end
|
||||
|
||||
test "it works for Host header with port same as Endpoint", %{
|
||||
conn: conn
|
||||
} do
|
||||
endpoint = URI.parse(Endpoint.url())
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> set_host("#{endpoint.host}:#{endpoint.port}")
|
||||
|> EnsureHostMatchesPlug.call(%{})
|
||||
|
||||
assert conn.halted == false
|
||||
assert Map.get(conn.assigns, :valid_host_header, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -106,4 +106,38 @@ defmodule Pleroma.Web.Plugs.RemoteIpTest do
|
|||
|
||||
assert conn.remote_ip == {1, 1, 1, 1}
|
||||
end
|
||||
|
||||
test "reserved ranges are configurable" do
|
||||
clear_config([RemoteIp, :reserved], [])
|
||||
|
||||
conn =
|
||||
conn(:get, "/")
|
||||
|> put_req_header("x-forwarded-for", "1.1.1.1, 10.0.0.3")
|
||||
|> RemoteIp.call(nil)
|
||||
|
||||
assert conn.remote_ip == {10, 0, 0, 3}
|
||||
end
|
||||
|
||||
test "clients override reserved ranges" do
|
||||
clear_config([RemoteIp, :clients], ["10.0.0.0/8"])
|
||||
|
||||
conn =
|
||||
conn(:get, "/")
|
||||
|> put_req_header("x-forwarded-for", "1.1.1.1, 10.0.0.3")
|
||||
|> RemoteIp.call(nil)
|
||||
|
||||
assert conn.remote_ip == {10, 0, 0, 3}
|
||||
end
|
||||
|
||||
test "clients override proxies" do
|
||||
clear_config([RemoteIp, :clients], ["10.0.0.3"])
|
||||
clear_config([RemoteIp, :proxies], ["10.0.0.0/8"])
|
||||
|
||||
conn =
|
||||
conn(:get, "/")
|
||||
|> put_req_header("x-forwarded-for", "1.1.1.1, 10.0.0.3")
|
||||
|> RemoteIp.call(nil)
|
||||
|
||||
assert conn.remote_ip == {10, 0, 0, 3}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -28,6 +28,15 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
|
|||
assert html_response(conn, 200) =~ user.nickname
|
||||
end
|
||||
|
||||
test "renders profile HTML inside the default app layout", %{conn: conn, user: user} do
|
||||
conn = get(conn, "/users/#{user.nickname}")
|
||||
|
||||
html = html_response(conn, 200)
|
||||
assert html =~ "<!DOCTYPE html>"
|
||||
assert html =~ ~s(class="instance-header")
|
||||
assert html =~ ~s(<link rel="stylesheet" href="/instance/static.css">)
|
||||
end
|
||||
|
||||
test "404 when user not found", %{conn: conn} do
|
||||
conn = get(conn, "/users/limpopo")
|
||||
|
||||
|
|
|
|||
|
|
@ -16,12 +16,13 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do
|
|||
alias Pleroma.Signature
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.UserView
|
||||
alias Pleroma.Web.Endpoint
|
||||
alias Pleroma.Web.Federator
|
||||
alias Pleroma.Workers.SignatureRetryWorker
|
||||
|
||||
defp signature_headers_for(%User{} = signer) do
|
||||
[
|
||||
{"host", "local.test"},
|
||||
{"host", "#{URI.parse(Endpoint.url()).host}"},
|
||||
{"date", "Thu, 25 Jul 2024 13:33:31 GMT"},
|
||||
{"digest", "SHA-256=fake-digest"},
|
||||
{"content-type", "application/activity+json"},
|
||||
|
|
@ -245,6 +246,66 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do
|
|||
refute Activity.get_by_ap_id(create["id"])
|
||||
end
|
||||
|
||||
test "cancels when the Host header does not match Endpoint" do
|
||||
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
|
||||
|
||||
create = %{
|
||||
"type" => "Create",
|
||||
"actor" => alice.ap_id,
|
||||
"id" => "https://one.com/activities/invalid-signature-create",
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => [],
|
||||
"object" => %{
|
||||
"type" => "Note",
|
||||
"id" => "https://one.com/objects/invalid-signature-note",
|
||||
"actor" => alice.ap_id,
|
||||
"attributedTo" => alice.ap_id,
|
||||
"content" => "forged post",
|
||||
"published" => "2024-07-25T13:33:31Z",
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc" => []
|
||||
}
|
||||
}
|
||||
|
||||
expect_signature_from(alice)
|
||||
|
||||
headers =
|
||||
[
|
||||
{"host", "invalid.example.com"},
|
||||
{"date", "Thu, 25 Jul 2024 13:33:31 GMT"},
|
||||
{"digest", "SHA-256=fake-digest"},
|
||||
{"content-type", "application/activity+json"},
|
||||
{
|
||||
"signature",
|
||||
"keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
|
||||
}
|
||||
]
|
||||
|
||||
assert {:ok, oban_job} =
|
||||
Federator.incoming_failed_signature_ap_doc(%{
|
||||
method: "POST",
|
||||
req_headers: headers,
|
||||
request_path: "/inbox",
|
||||
params: create,
|
||||
query_string: ""
|
||||
})
|
||||
|
||||
log =
|
||||
capture_log([level: :warning], fn ->
|
||||
assert {:cancel, :host_header_mismatch} = SignatureRetryWorker.perform(oban_job)
|
||||
end)
|
||||
|
||||
assert log =~ "Failed-signature inbox retry rejected"
|
||||
assert log =~ "reason=:host_header_mismatch"
|
||||
assert log =~ "payload_actor=\"https://one.com/users/alice\""
|
||||
assert log =~ "signature_actor=\"https://one.com/users/alice\""
|
||||
assert log =~ "activity_id=\"https://one.com/activities/invalid-signature-create\""
|
||||
assert log =~ "type=\"Create\""
|
||||
assert log =~ "request_path=\"/inbox\""
|
||||
|
||||
refute Activity.get_by_ap_id(create["id"])
|
||||
end
|
||||
|
||||
test "processes the activity after refetching a valid matching signature" do
|
||||
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
|
||||
|
||||
|
|
@ -309,11 +370,11 @@ defmodule Pleroma.Workers.SignatureRetryWorkerTest do
|
|||
"content-type" => "application/activity+json",
|
||||
date: date,
|
||||
digest: digest,
|
||||
host: "local.test"
|
||||
host: "#{URI.parse(Endpoint.url()).host}"
|
||||
})
|
||||
|
||||
req_headers = [
|
||||
["host", "local.test"],
|
||||
["host", "#{URI.parse(Endpoint.url()).host}"],
|
||||
["date", date],
|
||||
["digest", digest],
|
||||
["content-type", "application/activity+json"],
|
||||
|
|
|
|||
|
|
@ -119,7 +119,10 @@ defmodule Pleroma.Web.ConnCase do
|
|||
DataCase.stub_pipeline()
|
||||
|
||||
Mox.verify_on_exit!()
|
||||
endpoint = URI.parse(Pleroma.Web.Endpoint.url())
|
||||
conn = Phoenix.ConnTest.build_conn()
|
||||
conn = %{conn | req_headers: [{"host", "#{endpoint.host}:#{endpoint.port}"}]}
|
||||
|
||||
{:ok, conn: Phoenix.ConnTest.build_conn()}
|
||||
{:ok, conn: conn}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue