Merge branch 'develop' into audit-mix-audit-report

This commit is contained in:
Phantasm 2026-08-19 22:20:55 +02:00
commit 45459c8398
No known key found for this signature in database
GPG key ID: 2669E588BCC634C8
156 changed files with 8723 additions and 731 deletions

View file

@ -0,0 +1,5 @@
import Config
config :pleroma, Pleroma.Search, module: Pleroma.Search.ParadeDB
config :pleroma, Pleroma.Search.ParadeDB, url: "postgres://localhost/paradedb"
config :pleroma, Pleroma.Search.ParadeDB.Repo, pool_size: 2

View file

@ -0,0 +1,32 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/attachment-embedded-note",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:43:01Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-attachment-embedded-note",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:43:01Z",
"content": "Note with embedded Note attachment",
"attachment": {
"type": "Note",
"attributedTo": "https://fedidev.fun/users/alice",
"name": "attached note",
"content": "This is just a note",
"published": "2024-03-06T07:23:56Z"
}
}
}

View file

@ -0,0 +1,31 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/attachment-payto-link",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:43:00Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-attachment-payto-link",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:43:00Z",
"content": "Payment Link",
"attachment": {
"type": "Link",
"name": "Donate",
"href": "payto://iban/DE75512108001245126199",
"rel": "payment"
}
}
}

View file

@ -0,0 +1,40 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/attachment-url-list",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:43:02Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-attachment-url-list",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:43:02Z",
"content": "url is array of two Link objects",
"attachment": {
"type": "Image",
"url": [
{
"type": "Link",
"href": "https://fedidev.fun/images/007.png",
"mediaType": "image/png"
},
{
"type": "Link",
"href": "https://fedidev.fun/images/008.jpg",
"mediaType": "image/jpeg"
}
]
}
}
}

View file

@ -0,0 +1,32 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/emoji-icon-string",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:00Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-emoji-icon-string",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:00Z",
"content": "emoji icon as string :cow:",
"tag": [
{
"type": "Emoji",
"name": ":cow:",
"icon": "https://fedidev.fun/static/cow.png"
}
]
}
}

View file

@ -0,0 +1,34 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/emoji-icon-url-missing-type",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:01Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-emoji-icon-url-missing-type",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:01Z",
"content": "emoji icon url missing type :cow2:",
"tag": [
{
"type": "Emoji",
"name": ":cow2:",
"icon": {
"url": "https://fedidev.fun/static/cow.png"
}
}
]
}
}

View file

@ -0,0 +1,31 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/emoji-missing-icon",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:02Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-emoji-missing-icon",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:02Z",
"content": "emoji missing icon :cow3:",
"tag": [
{
"type": "Emoji",
"name": ":cow3:"
}
]
}
}

View file

@ -0,0 +1,30 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/in-reply-to-collection",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:08Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-in-reply-to-collection",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:08Z",
"content": "collection",
"inReplyTo": {
"type": "Collection",
"items": [
"https://fedidev.fun/users/alice"
]
}
}
}

View file

@ -0,0 +1,35 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/in-reply-to-embedded-note",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:21Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-in-reply-to-embedded-note",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:21Z",
"content": "embedded note",
"inReplyTo": {
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Note",
"id": "https://fedidev.fun/assets/note1.jsonap",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"attributedTo": "https://fedidev.fun/users/alice",
"content": "One",
"published": "2024-01-06T13:11:45Z"
}
}
}

View file

@ -0,0 +1,28 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/in-reply-to-link",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:39:56Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-in-reply-to-link",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:39:56Z",
"content": "Link in inReplyTo",
"inReplyTo": {
"type": "Link",
"href": "https://fedidev.fun/assets/note1.jsonap"
}
}
}

View file

@ -0,0 +1,28 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/in-reply-to-list",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:08Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-in-reply-to-list",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:08Z",
"content": "two elements in inReplyTo",
"inReplyTo": [
"https://fedidev.fun/assets/note1.jsonap",
"https://fedidev.fun/assets/note2.jsonap"
]
}
}

View file

@ -0,0 +1,25 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/missing-attributedto",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:42:00Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-missing-attributedto",
"attributedTo": null,
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:42:00Z",
"content": "object without attributedTo"
}
}

View file

@ -0,0 +1,28 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/misskey-quote-list",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:43:53Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-misskey-quote-list",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:43:53Z",
"content": "_misskey_quote attribute as list",
"_misskey_quote": [
"https://fedidev.fun/assets/note1.jsonap",
"https://fedidev.fun/assets/note2.jsonap"
]
}
}

View file

@ -0,0 +1,28 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/object-type-list",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:43:32Z",
"object": {
"type": [
"Note"
],
"id": "https://fedidev.fun/objects/note-object-type-list",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:43:32Z",
"name": "name",
"summary": "summary",
"content": "content"
}
}

View file

@ -0,0 +1,28 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/quote-uri-list",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:43:52Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-quote-uri-list",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:43:52Z",
"content": "quoteUri attribute as list",
"quoteUri": [
"https://fedidev.fun/assets/note1.jsonap",
"https://fedidev.fun/assets/note2.jsonap"
]
}
}

View file

@ -0,0 +1,28 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/quote-url-list",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:43:52Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-quote-url-list",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:43:52Z",
"content": "quoteUrl attribute as list",
"quoteUrl": [
"https://fedidev.fun/assets/note1.jsonap",
"https://fedidev.fun/assets/note2.jsonap"
]
}
}

View file

@ -0,0 +1,30 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/tag-hashtag-missing-type",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:41:00Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-tag-hashtag-missing-type",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:41:00Z",
"content": "hashtag without type",
"tag": [
{
"name": "#Test"
}
]
}
}

View file

@ -0,0 +1,31 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/tag-mention-missing-type",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:41:01Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-tag-mention-missing-type",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:41:01Z",
"content": "mention without type",
"tag": [
{
"name": "@bob",
"href": "https://remote.example/users/bob"
}
]
}
}

27
test/fixtures/fedidev.fun/url_list.json vendored Normal file
View file

@ -0,0 +1,27 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/url-list",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:39:06Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-url-list",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:39:06Z",
"content": "text",
"url": [
"https://fedidev.fun/objects/123"
]
}
}

View file

@ -87,6 +87,14 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
config = ConfigDB.get_by_params(%{group: ":pleroma", key: ":first_setting"})
assert config.value == [key: "value", key2: [Repo]]
end
test "static search settings remain in the config file" do
MixTask.migrate_to_db("test/fixtures/config/static_search.config")
refute ConfigDB.get_by_group_and_key(:pleroma, Pleroma.Search)
refute ConfigDB.get_by_group_and_key(:pleroma, Pleroma.Search.ParadeDB)
refute ConfigDB.get_by_group_and_key(:pleroma, Pleroma.Search.ParadeDB.Repo)
end
end
describe "with deletion of temp file" do

View file

@ -417,7 +417,8 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
["user_invite_tokens"],
["user_notes"],
["user_relationships"],
["users"]
["users"],
["webhooks"]
]
end

View file

@ -71,6 +71,31 @@ defmodule Pleroma.Config.TransferTaskTest do
assert assets_env[:mascots] == [a: 1, b: 2]
end
test "does not transfer static search configuration" do
search_config = Application.get_env(:pleroma, Pleroma.Search)
parade_config = Application.get_env(:pleroma, Pleroma.Search.ParadeDB)
clear_config(:database_config_blacklist, [])
insert(:config, key: Pleroma.Search, value: [module: Pleroma.Search.ParadeDB])
insert(:config,
key: Pleroma.Search.ParadeDB,
value: [url: "postgres://localhost/paradedb"]
)
insert(:config,
key: Pleroma.Search.ParadeDB.Repo,
value: [pool_size: 50]
)
TransferTask.start_link([])
assert Application.get_env(:pleroma, Pleroma.Search) == search_config
assert Application.get_env(:pleroma, Pleroma.Search.ParadeDB) == parade_config
assert Application.get_env(:pleroma, Pleroma.Search.ParadeDB.Repo)[:pool_size] == 2
end
describe "pleroma restart" do
setup do
on_exit(fn ->

View file

@ -36,6 +36,20 @@ defmodule Pleroma.ConfigDBTest do
assert config[:goose][:webhook_url] == "https://gander.com/"
end
test "get_all_as_keyword/1 excludes static search configuration" do
insert(:config, key: Pleroma.Search, value: [module: Pleroma.Search.ParadeDB])
insert(:config, key: Pleroma.Search.ParadeDB, value: [url: "postgres://example/db"])
insert(:config, key: Pleroma.Search.ParadeDB.Repo, value: [pool_size: 50])
insert(:config, key: :instance, value: [name: "Pleroma"])
config = ConfigDB.get_all_as_keyword(exclude_static: true)
refute config[:pleroma][Pleroma.Search]
refute config[:pleroma][Pleroma.Search.ParadeDB]
refute config[:pleroma][Pleroma.Search.ParadeDB.Repo]
assert config[:pleroma][:instance] == [name: "Pleroma"]
end
describe "update_or_create/1" do
test "common" do
config = insert(:config)

View file

@ -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"

View file

@ -0,0 +1,174 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Repo.OptionalMigrations.RebasedRollbacksTest do
use ExUnit.Case, async: false
alias Ecto.Adapters.SQL
defmodule MigrationRepo do
use Ecto.Repo,
otp_app: :pleroma,
adapter: Ecto.Adapters.Postgres
end
@migrations_path "priv/repo/migrations"
@rollbacks_path "priv/repo/optional_migrations/rebased_rollbacks"
@expected_versions ~w{
20210612185407
20220314220000
20220819171321
20220927220033
20221207235104
}
test "only includes Rebased-specific migration versions" do
rollback_versions = migration_versions(@rollbacks_path)
pleroma_versions = migration_versions(@migrations_path)
assert rollback_versions == @expected_versions
assert MapSet.disjoint?(MapSet.new(rollback_versions), MapSet.new(pleroma_versions))
end
test "rolls back Rebased schema without removing Pleroma data" do
prefix = "rebased_rollbacks_#{Ecto.UUID.generate() |> String.replace("-", "")}"
repo_config = [
username: "postgres",
password: "postgres",
database: System.get_env("DB_NAME", "pleroma_test"),
hostname: System.get_env("DB_HOST", "localhost"),
port: String.to_integer(System.get_env("DB_PORT", "5432")),
pool_size: 2,
parameters: [search_path: prefix]
]
{:ok, repo} = MigrationRepo.start_link(repo_config)
Process.unlink(repo)
on_exit(fn ->
query!("DROP SCHEMA IF EXISTS #{prefix} CASCADE")
MigrationRepo.stop()
end)
query!("CREATE SCHEMA #{prefix}")
create_rebased_schema()
expected_rollbacks =
@expected_versions
|> Enum.reverse()
|> Enum.map(&String.to_integer/1)
assert Ecto.Migrator.run(MigrationRepo, @rollbacks_path, :down, all: true) ==
expected_rollbacks
assert query!("SELECT version FROM schema_migrations ORDER BY version").rows == [
[20_220_624_104_914],
[20_221_029_171_353]
]
assert query!("""
SELECT column_name
FROM information_schema.columns
WHERE table_schema = current_schema() AND table_name = 'users'
ORDER BY column_name
""").rows == [["id"]]
assert query!("SELECT type::text FROM notifications ORDER BY type::text").rows == [["status"]]
assert query!("""
SELECT enumlabel
FROM pg_enum
JOIN pg_type ON pg_type.oid = enumtypid
JOIN pg_namespace ON pg_namespace.oid = pg_type.typnamespace
WHERE typname = 'notification_type' AND nspname = current_schema()
ORDER BY enumsortorder
""").rows ==
Enum.map(
~w{follow follow_request mention move pleroma:emoji_reaction pleroma:chat_mention reblog favourite pleroma:report poll status update},
&[&1]
)
assert query!("SELECT to_regclass('objects_events')").rows == [[nil]]
assert query!("SELECT url, secret, internal FROM webhooks").rows == [
["https://example.com/webhook", "preserve-me", false]
]
end
defp migration_versions(path) do
path
|> Path.join("*.exs")
|> Path.wildcard()
|> Enum.map(fn migration ->
migration
|> Path.basename()
|> String.split("_", parts: 2)
|> hd()
end)
|> Enum.sort()
end
defp create_rebased_schema do
statements = [
"""
CREATE TYPE notification_type AS ENUM (
'follow', 'follow_request', 'mention', 'move', 'pleroma:emoji_reaction',
'pleroma:chat_mention', 'reblog', 'favourite', 'pleroma:report', 'poll',
'status', 'update', 'pleroma:participation_accepted',
'pleroma:participation_request', 'pleroma:event_reminder',
'pleroma:event_update'
)
""",
"CREATE TABLE notifications (id bigserial PRIMARY KEY, type notification_type)",
"""
INSERT INTO notifications (type) VALUES
('status'),
('pleroma:participation_accepted'),
('pleroma:participation_request'),
('pleroma:event_reminder'),
('pleroma:event_update')
""",
"""
CREATE TABLE users (
id bigserial PRIMARY KEY,
accepts_email_list boolean,
location varchar(255),
last_move_at timestamp
)
""",
"CREATE TABLE objects (id bigserial PRIMARY KEY, data jsonb)",
"CREATE INDEX objects_events ON objects ((data->>'type')) WHERE data->>'type' = 'Event'",
"""
CREATE TABLE webhooks (
id bigserial PRIMARY KEY,
url varchar(255) NOT NULL,
events varchar[] NOT NULL DEFAULT '{}',
secret varchar(255) NOT NULL DEFAULT '',
enabled boolean NOT NULL DEFAULT true,
internal boolean NOT NULL DEFAULT false
)
""",
"CREATE UNIQUE INDEX webhooks_url_index ON webhooks (url)",
"INSERT INTO webhooks (url, secret) VALUES ('https://example.com/webhook', 'preserve-me')",
"CREATE TABLE schema_migrations (version bigint PRIMARY KEY, inserted_at timestamp(0))",
"""
INSERT INTO schema_migrations (version, inserted_at) VALUES
(20210612185407, NOW()),
(20220314220000, NOW()),
(20220624104914, NOW()),
(20220819171321, NOW()),
(20220927220033, NOW()),
(20221029171353, NOW()),
(20221207235104, NOW())
"""
]
Enum.each(statements, &query!/1)
end
defp query!(statement), do: SQL.query!(MigrationRepo, statement, [])
end

View file

@ -11,6 +11,15 @@ defmodule Pleroma.ReverseProxyTest do
alias Pleroma.ReverseProxy.ClientMock
alias Plug.Conn
defmodule ProxyPlug do
@moduledoc false
def init(options), do: options
def call(conn, {url, opts}), do: Pleroma.ReverseProxy.call(conn, url, opts)
def call(conn, url), do: Pleroma.ReverseProxy.call(conn, url)
end
setup_all do
{:ok, _} = Registry.start_link(keys: :unique, name: ClientMock)
:ok
@ -33,6 +42,88 @@ defmodule Pleroma.ReverseProxyTest do
:ok
end
defp start_bandit(url, opts \\ []) do
start_supervised!(
{Bandit,
plug: {ProxyPlug, {url, opts}},
ip: {127, 0, 0, 1},
port: 0,
http_options: [log_exceptions_with_status_codes: []]}
)
end
defp start_cowboy(url, opts \\ []) do
ref = {__MODULE__, make_ref()}
{:ok, _pid} =
Plug.Cowboy.http(ProxyPlug, {url, opts},
ip: {127, 0, 0, 1},
port: 0,
ref: ref,
protocol_options: [stream_handlers: [:cowboy_stream_h]]
)
on_exit(fn -> Plug.Cowboy.shutdown(ref) end)
:ranch.get_port(ref)
end
defp bandit_request(url, headers \\ [], opts \\ []) do
pid = start_bandit(url, opts)
{:ok, {{127, 0, 0, 1}, port}} = ThousandIsland.listener_info(pid)
http1_request(port, headers)
end
defp cowboy_request(url, headers \\ [], opts \\ []) do
url
|> start_cowboy(opts)
|> http1_request(headers)
end
defp http1_request(port, headers) do
{:ok, socket} = :gen_tcp.connect({127, 0, 0, 1}, port, [:binary, active: false])
request_headers =
[["host: localhost", "connection: close"] | headers]
|> List.flatten()
|> Enum.join("\r\n")
:ok = :gen_tcp.send(socket, "GET / HTTP/1.1\r\n#{request_headers}\r\n\r\n")
recv_all(socket, [])
end
defp recv_all(socket, chunks) do
case :gen_tcp.recv(socket, 0, 5_000) do
{:ok, data} -> recv_all(socket, [data | chunks])
{:error, reason} -> {reason, chunks |> Enum.reverse() |> IO.iodata_to_binary()}
end
end
defp receive_mint_responses(conn, responses \\ []) do
receive do
message ->
case Mint.HTTP2.stream(conn, message) do
{:ok, conn, new_responses} ->
responses = responses ++ new_responses
if Enum.any?(new_responses, fn response ->
elem(response, 0) in [:done, :error]
end) do
responses
else
receive_mint_responses(conn, responses)
end
{:error, _conn, error, new_responses} ->
responses ++ new_responses ++ [{:connection_error, error}]
:unknown ->
receive_mint_responses(conn, responses)
end
after
2_000 -> responses ++ [:timeout]
end
end
defp request_mock(invokes) do
ClientMock
|> expect(:request, fn :get, url, headers, _body, _opts ->
@ -58,6 +149,45 @@ defmodule Pleroma.ReverseProxyTest do
end)
end
defp stream_then_error_mock(url, headers, chunk \\ "partial", error \\ :closed) do
ClientMock
|> expect(:request, fn :get, ^url, _, _, _ ->
Registry.register(ClientMock, url, 0)
{:ok, 200, headers, %{url: url}}
end)
|> expect(:stream_body, 2, fn %{url: ^url} = client ->
case Registry.lookup(ClientMock, url) do
[{_, 0}] ->
Registry.update_value(ClientMock, url, &(&1 + 1))
{:ok, chunk, client}
[{_, 1}] ->
Registry.unregister(ClientMock, url)
{:error, error}
end
end)
|> expect(:close, fn _ -> :ok end)
end
defp complete_stream_mock(url, headers, body) do
ClientMock
|> expect(:request, fn :get, ^url, _, _, _ ->
Registry.register(ClientMock, url, 0)
{:ok, 200, headers, %{url: url}}
end)
|> expect(:stream_body, 2, fn %{url: ^url} = client ->
case Registry.lookup(ClientMock, url) do
[{_, 0}] ->
Registry.update_value(ClientMock, url, &(&1 + 1))
{:ok, body, client}
[{_, 1}] ->
Registry.unregister(ClientMock, url)
:done
end
end)
end
describe "reverse proxy" do
test "do not track successful request", %{conn: conn} do
request_mock(2)
@ -179,14 +309,202 @@ defmodule Pleroma.ReverseProxyTest do
assert response == %{"user-agent" => Pleroma.Application.user_agent()}
end
test "closed connection", %{conn: conn} do
ClientMock
|> expect(:request, fn :get, "/closed", _, _, _ -> {:ok, 200, [], %{}} end)
|> expect(:stream_body, fn _ -> {:error, :closed} end)
|> expect(:close, fn _ -> :ok end)
test "aborts a partially streamed response when the upstream closes", %{conn: conn} do
stream_then_error_mock("/closed", [{"content-length", "20"}])
conn = ReverseProxy.call(conn, "/closed")
assert conn.halted
assert_raise ReverseProxy.StreamError, fn ->
ReverseProxy.call(conn, "/closed")
end
assert Cachex.get(:failed_proxy_url_cache, "/closed") == {:ok, true}
end
describe "Bandit HTTP/1 streaming" do
test "closes an incomplete chunked response without a terminating chunk" do
Mox.set_mox_global(ClientMock)
stream_then_error_mock("/bandit-closed", [{"content-type", "image/png"}])
assert {:closed, response} = bandit_request("/bandit-closed")
assert response =~ "transfer-encoding: chunked"
assert response =~ "partial"
refute response =~ "0\r\n\r\n"
end
test "closes an incomplete fixed-length response before the declared length" do
Mox.set_mox_global(ClientMock)
stream_then_error_mock("/bandit-fixed-closed", [
{"content-length", "20"},
{"content-type", "image/png"}
])
assert {:closed, response} = bandit_request("/bandit-fixed-closed")
assert response =~ "content-length: 20"
refute response =~ "transfer-encoding"
assert String.ends_with?(response, "partial")
end
test "streams a complete fixed-length response without transformation" do
Mox.set_mox_global(ClientMock)
body = "complete body"
complete_stream_mock(
"/bandit-complete",
[
{"content-length", to_string(byte_size(body))},
{"content-type", "image/png"}
],
body
)
assert {:closed, response} =
bandit_request("/bandit-complete", ["accept-encoding: deflate"])
assert response =~ "content-length: #{byte_size(body)}"
assert response =~ "cache-control: public, max-age=1209600, immutable, no-transform"
refute response =~ "content-encoding: gzip"
refute response =~ "content-encoding: deflate"
assert String.ends_with?(response, body)
end
test "keeps no-transform when custom response headers replace cache-control" do
Mox.set_mox_global(ClientMock)
body = "complete body"
complete_stream_mock(
"/bandit-custom-cache-control",
[
{"content-length", to_string(byte_size(body))},
{"content-type", "image/png"}
],
body
)
assert {:closed, response} =
bandit_request(
"/bandit-custom-cache-control",
["accept-encoding: deflate"],
resp_headers: [{"cache-control", "private, max-age=60"}]
)
assert response =~ "content-length: #{byte_size(body)}"
assert response =~ "cache-control: private, max-age=60, no-transform"
refute response =~ "content-encoding: gzip"
refute response =~ "content-encoding: deflate"
assert String.ends_with?(response, body)
end
test "does not blacklist the upstream when the downstream disconnects" do
Mox.set_mox_global(ClientMock)
test_pid = self()
ClientMock
|> expect(:request, fn :get, "/bandit-client-closed", _, _, _ ->
{:ok, 200, [{"content-type", "image/png"}], %{url: "/bandit-client-closed"}}
end)
|> stub(:stream_body, fn client ->
send(test_pid, {:stream_requested, self()})
receive do
:continue -> {:ok, :binary.copy("x", 1_000_000), client}
after
1_000 -> raise "timed out waiting for downstream disconnect"
end
end)
|> expect(:close, fn _ ->
send(test_pid, :upstream_closed)
:ok
end)
pid = start_bandit("/bandit-client-closed")
{:ok, {{127, 0, 0, 1}, port}} = ThousandIsland.listener_info(pid)
{:ok, socket} = :gen_tcp.connect({127, 0, 0, 1}, port, [:binary, active: false])
:ok = :gen_tcp.send(socket, "GET / HTTP/1.1\r\nhost: localhost\r\n\r\n")
assert_receive {:stream_requested, request_pid}
:ok = :inet.setopts(socket, linger: {true, 0})
:ok = :gen_tcp.close(socket)
send(request_pid, :continue)
assert_receive :upstream_closed
assert Cachex.get(:failed_proxy_url_cache, "/bandit-client-closed") == {:ok, nil}
end
end
describe "Cowboy streaming" do
test "closes an incomplete HTTP/1 chunked response without a terminating chunk" do
Mox.set_mox_global(ClientMock)
stream_then_error_mock("/cowboy-closed", [{"content-type", "image/png"}])
assert {:closed, response} = cowboy_request("/cowboy-closed")
assert response =~ "transfer-encoding: chunked"
assert response =~ "partial"
refute response =~ "0\r\n\r\n"
end
test "closes an incomplete HTTP/1 fixed-length response before the declared length" do
Mox.set_mox_global(ClientMock)
stream_then_error_mock("/cowboy-fixed-closed", [
{"content-length", "20"},
{"content-type", "image/png"}
])
assert {:closed, response} = cowboy_request("/cowboy-fixed-closed")
assert response =~ "content-length: 20"
refute response =~ "transfer-encoding"
assert String.ends_with?(response, "partial")
end
test "resets an incomplete HTTP/2 stream" do
Mox.set_mox_global(ClientMock)
stream_then_error_mock("/cowboy-http2-closed", [{"content-type", "image/png"}])
port = start_cowboy("/cowboy-http2-closed")
{:ok, mint} = Mint.HTTP2.connect(:http, "localhost", port)
{:ok, mint, request_ref} = Mint.HTTP2.request(mint, "GET", "/", [], nil)
responses = receive_mint_responses(mint)
assert {:status, request_ref, 200} in responses
assert {:data, request_ref, "partial"} in responses
assert Enum.any?(responses, fn
{:error, ^request_ref, %Mint.HTTPError{reason: {:server_closed_request, _}}} ->
true
_ ->
false
end)
refute {:done, request_ref} in responses
refute :timeout in responses
end
end
test "resets an incomplete Bandit HTTP/2 stream" do
Mox.set_mox_global(ClientMock)
stream_then_error_mock("/bandit-http2-closed", [{"content-type", "image/png"}])
pid = start_bandit("/bandit-http2-closed")
{:ok, {{127, 0, 0, 1}, port}} = ThousandIsland.listener_info(pid)
{:ok, mint} = Mint.HTTP2.connect(:http, "localhost", port)
{:ok, mint, request_ref} = Mint.HTTP2.request(mint, "GET", "/", [], nil)
responses = receive_mint_responses(mint)
assert {:status, request_ref, 200} in responses
assert {:data, request_ref, "partial"} in responses
assert Enum.any?(responses, fn
{:error, ^request_ref,
%Mint.HTTPError{reason: {:server_closed_request, :internal_error}}} ->
true
_ ->
false
end)
refute {:done, request_ref} in responses
refute :timeout in responses
end
defp stream_mock(invokes) do
@ -247,9 +565,14 @@ defmodule Pleroma.ReverseProxyTest do
test "max_body_length returns error if streaming body more than that option", %{conn: conn} do
stream_mock(3)
assert capture_log(fn ->
ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 29)
end) =~
log =
capture_log(fn ->
assert_raise ReverseProxy.StreamError, fn ->
ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 29)
end
end)
assert log =~
"Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large"
assert_received {:closed, %{url: "/stream-bytes/50"}}
@ -277,6 +600,19 @@ defmodule Pleroma.ReverseProxyTest do
assert Conn.get_resp_header(conn, "content-type") == ["image/png"]
assert conn.resp_body == ""
end
test "rejects a malformed content-length", %{conn: conn} do
url = "/head-invalid-content-length"
expect(ClientMock, :request, fn :head, ^url, _, _, _ ->
{:ok, 200, [{"content-length", "20junk"}]}
end)
conn = ReverseProxy.call(Map.put(conn, :method, "HEAD"), url)
assert conn.status == 500
assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
end
end
defp error_mock(status) when is_integer(status) do
@ -351,34 +687,6 @@ 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"
@ -473,7 +781,73 @@ defmodule Pleroma.ReverseProxyTest do
|> expect(:stream_body, fn _ -> :done end)
conn = ReverseProxy.call(conn, "/cache")
assert {"cache-control", "public, max-age=1209600, immutable"} in conn.resp_headers
assert {"cache-control", "public, max-age=1209600, immutable, no-transform"} in conn.resp_headers
end
test "preserves a valid upstream content-length", %{conn: conn} do
body = "complete body"
ClientMock
|> expect(:request, fn :get, "/content-length", _, _, _ ->
{:ok, 200, [{"content-length", to_string(byte_size(body))}], %{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, "/content-length")
assert Conn.get_resp_header(conn, "content-length") == [to_string(byte_size(body))]
end
test "rejects malformed upstream content-length values" do
Enum.each(["20junk", "-1"], fn content_length ->
url = "/invalid-content-length/#{content_length}"
ClientMock
|> expect(:request, fn :get, ^url, _, _, _ ->
{:ok, 200, [{"content-length", content_length}], %{url: url}}
end)
|> expect(:close, fn %{url: ^url} -> :ok end)
conn = ReverseProxy.call(build_conn(), url)
assert conn.status == 500
assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
end)
end
test "rejects conflicting upstream content-length values" do
url = "/conflicting-content-length"
ClientMock
|> expect(:request, fn :get, ^url, _, _, _ ->
{:ok, 200, [{"content-length", "7"}, {"content-length", "20"}], %{url: url}}
end)
|> stub(:stream_body, fn _ -> :done end)
|> stub(:close, fn _ -> :ok end)
conn = ReverseProxy.call(build_conn(), url)
assert conn.status == 500
assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
end
test "normalizes equivalent upstream content-length values", %{conn: conn} do
body = "partial"
complete_stream_mock(
"/duplicate-content-length",
[{"content-length", "7"}, {"content-length", "07"}],
body
)
conn = ReverseProxy.call(conn, "/duplicate-content-length")
assert Conn.get_resp_header(conn, "content-length") == ["7"]
assert conn.resp_body == body
end
end
@ -507,24 +881,6 @@ 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
@ -537,50 +893,6 @@ 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

View file

@ -0,0 +1,79 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Search.BackfillTest do
use Pleroma.DataCase, async: false
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
alias Pleroma.Search.Backfill
alias Pleroma.Workers.SearchIndexingWorker
test "enqueues the exact requested number across partial pages" do
activities = insert_list(5, :note_activity) |> Enum.sort_by(& &1.id, :desc)
assert %{enqueued: 3, exhausted: false, next_cursor: cursor} =
Backfill.run(limit: 3, step: 2, chunk: 2)
assert search_indexing_ids() == Enum.map(Enum.take(activities, 3), & &1.id)
assert cursor == Enum.at(activities, 2).id
end
test "resumes after the last processed activity without duplicates" do
activities = insert_list(5, :note_activity) |> Enum.sort_by(& &1.id, :desc)
assert %{enqueued: 2, next_cursor: cursor} = Backfill.run(limit: 2, step: 1)
Pleroma.Repo.delete_all(Oban.Job)
assert %{enqueued: 2, next_cursor: next_cursor} =
Backfill.run(before: cursor, limit: 2, step: 1)
assert search_indexing_ids() ==
activities
|> Enum.slice(2, 2)
|> Enum.map(& &1.id)
assert next_cursor == Enum.at(activities, 3).id
end
test "only queues Create activities" do
create = insert(:note_activity)
insert(:like_activity, note_activity: create)
assert %{enqueued: 1, exhausted: true, next_cursor: cursor} = Backfill.run(step: 2)
assert search_indexing_ids() == [create.id]
assert cursor == create.id
end
test "rejects unknown options and malformed cursors before enqueueing" do
assert_raise ArgumentError, fn -> Backfill.run(limt: 1) end
assert_raise ArgumentError, fn -> Backfill.run(before: "not-an-activity-id") end
assert_raise ArgumentError, fn -> Backfill.run(on_page: :invalid) end
assert all_enqueued(worker: SearchIndexingWorker) == []
end
test "does not duplicate or skip the initial activities when newer rows are inserted" do
activities = insert_list(3, :note_activity) |> Enum.sort_by(& &1.id, :desc)
assert %{enqueued: 3, exhausted: true} =
Backfill.run(
limit: :infinity,
step: 1,
on_page: fn %{enqueued: enqueued} ->
if enqueued == 1, do: insert(:note_activity)
end
)
assert search_indexing_ids() == Enum.map(activities, & &1.id)
end
defp search_indexing_ids do
all_enqueued(worker: SearchIndexingWorker)
|> Enum.sort_by(& &1.id)
|> Enum.map(& &1.args["activity"])
end
end

View file

@ -0,0 +1,323 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Search.ParadeDBIntegrationTest do
use Pleroma.DataCase, async: false
@moduletag :integration
@moduletag timeout: 180_000
import Pleroma.Factory
alias Pleroma.Activity
alias Pleroma.Search.ParadeDB
alias Pleroma.Web.CommonAPI
@image System.get_env(
"PARADEDB_TEST_IMAGE",
"paradedb/paradedb:0.24.3-pg15@sha256:997d62f6c2605becb7c45878af3f8c8c698f355006b675b3ce212d4eb2d14f2e"
)
@db_user "postgres"
@db_password "postgres"
@db_name "paradedb"
@docker_startup_timeout_ms 60_000
setup do
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
:ok
end
setup_all do
case System.get_env("PARADEDB_TEST_URL") do
nil -> setup_docker_paradedb()
url -> setup_external_paradedb(url)
end
end
defp setup_docker_paradedb do
with :ok <- ensure_docker_available() do
container = "pleroma-paradedb-it-#{System.unique_integer([:positive])}"
on_exit(fn ->
_ = docker(["rm", "-f", container])
end)
{id, 0} =
docker([
"run",
"--rm",
"--name",
container,
"-e",
"POSTGRES_USER=#{@db_user}",
"-e",
"POSTGRES_PASSWORD=#{@db_password}",
"-e",
"POSTGRES_DB=#{@db_name}",
"-p",
"127.0.0.1::5432",
"-d",
@image
])
container_id = String.trim(id)
host_port =
container
|> wait_for_port!(@docker_startup_timeout_ms)
wait_for_pg_ready!(container, @docker_startup_timeout_ms)
url = "postgres://#{@db_user}:#{@db_password}@127.0.0.1:#{host_port}/#{@db_name}"
setup_repo(url, %{container: container, container_id: container_id})
end
end
defp setup_external_paradedb(url) do
uri = URI.parse(url)
wait_for_tcp_ready!(uri.host, uri.port || 5432, @docker_startup_timeout_ms)
setup_repo(url, %{})
end
defp setup_repo(url, context) do
prev_env = System.get_env("PARADEDB_DATABASE_URL")
System.put_env("PARADEDB_DATABASE_URL", url)
on_exit(fn ->
if is_nil(prev_env) do
System.delete_env("PARADEDB_DATABASE_URL")
else
System.put_env("PARADEDB_DATABASE_URL", prev_env)
end
end)
{:ok, _pid} = start_supervised({Pleroma.Search.ParadeDB.Repo, pool_size: 5})
wait_for_repo_ready!(@docker_startup_timeout_ms)
previous_table = Pleroma.Config.get([Pleroma.Search.ParadeDB, :table], nil)
suffix = Ecto.UUID.generate() |> String.replace("-", "")
table = "pleroma_search_documents_it_#{suffix}"
Pleroma.Config.put([Pleroma.Search.ParadeDB, :table], table)
on_exit(fn ->
if is_nil(previous_table) do
Pleroma.Config.delete([Pleroma.Search.ParadeDB, :table])
else
Pleroma.Config.put([Pleroma.Search.ParadeDB, :table], previous_table)
end
end)
{:ok, Map.merge(context, %{url: url, table: table})}
end
test "indexes and searches against real ParadeDB", %{table: table} do
on_exit(fn ->
_ =
Ecto.Adapters.SQL.query(
Pleroma.Search.ParadeDB.Repo,
"DROP TABLE IF EXISTS #{table}"
)
end)
:ok = ParadeDB.drop_index()
assert :ok = ParadeDB.create_index()
assert {:ok, %{rows: [["0.24.3"]]}} =
Ecto.Adapters.SQL.query(
Pleroma.Search.ParadeDB.Repo,
"SELECT extversion FROM pg_extension WHERE extname = 'pg_search'"
)
user = insert(:user)
{:ok, a1} =
CommonAPI.post(user, %{status: "paradedb integration first", visibility: "public"})
:timer.sleep(1100)
{:ok, a2} =
CommonAPI.post(user, %{status: "paradedb integration second", visibility: "public"})
{:ok, private} =
CommonAPI.post(user, %{status: "paradedb integration private", visibility: "private"})
a1 = Activity.get_by_id_with_object(a1.id)
a2 = Activity.get_by_id_with_object(a2.id)
private = Activity.get_by_id_with_object(private.id)
a1 = %{
a1
| object: %{
a1.object
| data:
Map.merge(a1.object.data, %{
"summary" => "paradedb subject phrase",
"attachment" => [
%{
"name" => "remote-image.jpg",
"summary" => "paradedb attachment description"
}
]
})
}
}
assert :ok = ParadeDB.add_to_index(a1)
assert :ok = ParadeDB.add_to_index(a2)
assert :ok = ParadeDB.add_to_index(private)
results = ParadeDB.search(nil, "paradedb integration", limit: 40, offset: 0)
ids = Enum.map(results, & &1.id)
assert a1.id in ids
assert a2.id in ids
refute private.id in ids
assert List.first(ids) == a2.id
assert Enum.any?(ParadeDB.search(nil, "subject phrase"), &(&1.id == a1.id))
assert Enum.any?(ParadeDB.search(nil, "attachment description"), &(&1.id == a1.id))
previous_fuzzy_distance = Pleroma.Config.get([Pleroma.Search.ParadeDB, :fuzzy_distance], 0)
Pleroma.Config.put([Pleroma.Search.ParadeDB, :fuzzy_distance], 1)
on_exit(fn ->
Pleroma.Config.put([Pleroma.Search.ParadeDB, :fuzzy_distance], previous_fuzzy_distance)
end)
fuzzy_results = ParadeDB.search(nil, "paradedb integraion", limit: 40, offset: 0)
fuzzy_ids = Enum.map(fuzzy_results, & &1.id)
assert a1.id in fuzzy_ids
assert a2.id in fuzzy_ids
refute private.id in fuzzy_ids
# Removing an object should remove it from the ParadeDB index.
assert :ok = ParadeDB.remove_from_index(a2.object)
results_after_delete = ParadeDB.search(nil, "paradedb integration", limit: 40, offset: 0)
ids_after_delete = Enum.map(results_after_delete, & &1.id)
assert a1.id in ids_after_delete
refute a2.id in ids_after_delete
end
defp ensure_docker_available do
if is_nil(System.find_executable("docker")) do
{:skip, "Docker not installed"}
else
case docker(["info"]) do
{_out, 0} -> :ok
{out, _} -> {:skip, "Docker not available: #{String.trim(out)}"}
end
end
end
defp wait_for_port!(container, timeout_ms) do
deadline = System.monotonic_time(:millisecond) + timeout_ms
wait = fn wait ->
case docker(["port", container, "5432/tcp"]) do
{out, 0} when byte_size(out) > 0 ->
out
|> String.split("\n", trim: true)
|> List.first()
|> parse_host_port!()
_ ->
if System.monotonic_time(:millisecond) > deadline do
raise "Timed out waiting for docker port mapping"
else
:timer.sleep(250)
wait.(wait)
end
end
end
wait.(wait)
end
defp parse_host_port!(mapping) do
mapping
|> String.trim()
|> String.split(":")
|> List.last()
|> String.to_integer()
end
defp wait_for_pg_ready!(container, timeout_ms) do
deadline = System.monotonic_time(:millisecond) + timeout_ms
wait = fn wait ->
case docker([
"exec",
container,
"pg_isready",
"-U",
@db_user,
"-d",
@db_name
]) do
{_out, 0} ->
:ok
_ ->
if System.monotonic_time(:millisecond) > deadline do
raise "Timed out waiting for ParadeDB Postgres to become ready"
else
:timer.sleep(250)
wait.(wait)
end
end
end
wait.(wait)
end
defp wait_for_tcp_ready!(host, port, timeout_ms) do
deadline = System.monotonic_time(:millisecond) + timeout_ms
wait = fn wait ->
case :gen_tcp.connect(String.to_charlist(host), port, [:binary, active: false], 500) do
{:ok, socket} ->
:gen_tcp.close(socket)
:ok
{:error, _} ->
if System.monotonic_time(:millisecond) > deadline do
raise "Timed out waiting for TCP port #{port} to accept connections"
else
:timer.sleep(250)
wait.(wait)
end
end
end
wait.(wait)
end
defp wait_for_repo_ready!(timeout_ms) do
deadline = System.monotonic_time(:millisecond) + timeout_ms
wait = fn wait ->
case Ecto.Adapters.SQL.query(Pleroma.Search.ParadeDB.Repo, "SELECT 1") do
{:ok, _} ->
:ok
_ ->
if System.monotonic_time(:millisecond) > deadline do
raise "Timed out waiting for ParadeDB Repo"
else
:timer.sleep(250)
wait.(wait)
end
end
end
wait.(wait)
end
defp docker(args) do
System.cmd("docker", args, stderr_to_stdout: true)
end
end

View file

@ -0,0 +1,286 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Search.ParadeDBTest do
use Pleroma.DataCase, async: true
use Oban.Testing, repo: Pleroma.Repo
import Mox
import Pleroma.Factory
alias Pleroma.Search.ParadeDB
alias Pleroma.Search.ParadeDB.ClientMock
alias Pleroma.UnstubbedConfigMock, as: Config
alias Pleroma.Web.CommonAPI
alias Pleroma.Workers.SearchIndexingWorker
describe "ParadeDB" do
test "indexes a public post on creation" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "guys i just don't wanna leave the swamp",
visibility: "public"
})
args = %{"op" => "add_to_index", "activity" => activity.id}
assert_enqueued(worker: SearchIndexingWorker, args: args)
ClientMock
|> expect(:query, fn sql, params ->
assert sql =~ "INSERT INTO pleroma_search_documents"
[activity_id, object_id, object_ap_id, actor_ap_id, content, published_at] = params
{:ok, dumped_activity_id} = FlakeId.Ecto.CompatType.dump(activity.id)
assert activity_id == dumped_activity_id
assert is_integer(object_id)
assert is_binary(object_ap_id)
assert actor_ap_id == activity.data["actor"]
assert content == "guys i just don&#39;t wanna leave the swamp"
assert %DateTime{} = published_at
send(self(), :inserted)
{:ok, %{}}
end)
Config
|> expect(:get, 3, fn
[Pleroma.Search, :module], nil ->
ParadeDB
[Pleroma.Search.ParadeDB, :client_impl], nil ->
ClientMock
[Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" ->
"pleroma_search_documents"
end)
assert :ok = perform_job(SearchIndexingWorker, args)
assert_received(:inserted)
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 "backend skips non-public posts when called directly" 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
})
assert :ok = ParadeDB.add_to_index(activity)
end)
end
test "deletes posts from index when deleted locally" do
user = insert(:user)
ClientMock
|> expect(:query, 2, fn sql, params ->
cond do
String.contains?(sql, "INSERT INTO pleroma_search_documents") ->
send(self(), :inserted)
{:ok, %{}}
String.contains?(sql, "DELETE FROM pleroma_search_documents") ->
assert [object_id] = params
assert is_integer(object_id)
send(self(), :deleted)
{:ok, %{}}
true ->
flunk("Unexpected ParadeDB SQL: #{inspect(sql)}")
end
end)
Config
|> expect(:get, 6, fn
[Pleroma.Search, :module], nil ->
ParadeDB
[Pleroma.Search.ParadeDB, :client_impl], nil ->
ClientMock
[Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" ->
"pleroma_search_documents"
end)
{:ok, activity} =
CommonAPI.post(user, %{
status: "guys i just don't wanna leave the swamp",
visibility: "public"
})
args = %{"op" => "add_to_index", "activity" => activity.id}
assert_enqueued(worker: SearchIndexingWorker, args: args)
assert :ok = perform_job(SearchIndexingWorker, args)
assert_received(:inserted)
{:ok, _} = CommonAPI.delete(activity.id, user)
delete_args = %{"op" => "remove_from_index", "object" => activity.object.id}
assert_enqueued(worker: SearchIndexingWorker, args: delete_args)
assert :ok = perform_job(SearchIndexingWorker, delete_args)
assert_received(:deleted)
end
test "search returns activities in backend order" do
user = insert(:user)
{:ok, activity1} = CommonAPI.post(user, %{status: "first swamp", visibility: "public"})
{:ok, activity2} = CommonAPI.post(user, %{status: "second swamp", visibility: "public"})
activity1_id = activity1.id
activity2_id = activity2.id
{:ok, dumped_activity1_id} = FlakeId.Ecto.CompatType.dump(activity1_id)
{:ok, dumped_activity2_id} = FlakeId.Ecto.CompatType.dump(activity2_id)
ClientMock
|> expect(:query, fn sql, params ->
assert sql =~ "SELECT id FROM pleroma_search_documents"
assert ["swamp", _limit, _offset] = params
{:ok, %{rows: [[dumped_activity2_id], [dumped_activity1_id]]}}
end)
Config
|> expect(:get, 3, fn
[Pleroma.Search.ParadeDB, :client_impl], nil ->
ClientMock
[Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" ->
"pleroma_search_documents"
[Pleroma.Search.ParadeDB, :fuzzy_distance], 0 ->
0
end)
assert [%{id: ^activity2_id}, %{id: ^activity1_id}] =
ParadeDB.search(nil, "swamp", limit: 40, offset: 0)
end
test "search excludes non-public activities from backend hits" do
user = insert(:user)
{:ok, public_activity} =
CommonAPI.post(user, %{status: "public swamp", visibility: "public"})
{:ok, private_activity} =
CommonAPI.post(user, %{status: "private swamp", visibility: "private"})
public_activity_id = public_activity.id
{:ok, dumped_public_id} = FlakeId.Ecto.CompatType.dump(public_activity.id)
{:ok, dumped_private_id} = FlakeId.Ecto.CompatType.dump(private_activity.id)
ClientMock
|> expect(:query, fn sql, params ->
assert sql =~ "SELECT id FROM pleroma_search_documents"
assert ["swamp", _limit, _offset] = params
{:ok, %{rows: [[dumped_private_id], [dumped_public_id]]}}
end)
Config
|> expect(:get, 3, fn
[Pleroma.Search.ParadeDB, :client_impl], nil ->
ClientMock
[Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" ->
"pleroma_search_documents"
[Pleroma.Search.ParadeDB, :fuzzy_distance], 0 ->
0
end)
assert [%{id: ^public_activity_id}] = ParadeDB.search(nil, "swamp", limit: 40, offset: 0)
end
test "search supports backend ids returned as uuid strings" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "uuid swamp", visibility: "public"})
activity_id = activity.id
{:ok, dumped_id} = FlakeId.Ecto.CompatType.dump(activity_id)
{:ok, uuid_string} = Ecto.UUID.load(dumped_id)
ClientMock
|> expect(:query, fn sql, params ->
assert sql =~ "SELECT id FROM pleroma_search_documents"
assert ["swamp", _limit, _offset] = params
{:ok, %{rows: [[uuid_string]]}}
end)
Config
|> expect(:get, 3, fn
[Pleroma.Search.ParadeDB, :client_impl], nil ->
ClientMock
[Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" ->
"pleroma_search_documents"
[Pleroma.Search.ParadeDB, :fuzzy_distance], 0 ->
0
end)
assert [%{id: ^activity_id}] = ParadeDB.search(nil, "swamp", limit: 40, offset: 0)
end
test "search uses configured fuzzy distance" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "runing shoes", visibility: "public"})
activity_id = activity.id
{:ok, dumped_id} = FlakeId.Ecto.CompatType.dump(activity_id)
ClientMock
|> expect(:query, fn sql, params ->
assert sql =~ "SELECT id FROM pleroma_search_documents"
assert sql =~ "($1::text)::pdb.fuzzy(1)"
assert ["running shoes", _limit, _offset] = params
{:ok, %{rows: [[dumped_id]]}}
end)
Config
|> expect(:get, 3, fn
[Pleroma.Search.ParadeDB, :client_impl], nil ->
ClientMock
[Pleroma.Search.ParadeDB, :table], "pleroma_search_documents" ->
"pleroma_search_documents"
[Pleroma.Search.ParadeDB, :fuzzy_distance], 0 ->
1
end)
assert [%{id: ^activity_id}] = ParadeDB.search(nil, "running shoes", limit: 40, offset: 0)
end
end
end

View file

@ -3,26 +3,32 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.SearchTest do
require Pleroma.Constants
use Pleroma.DataCase, async: true
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
alias Pleroma.Object
alias Pleroma.Search
alias Pleroma.Web.CommonAPI
alias Pleroma.Workers.SearchIndexingWorker
test "indexes posts that are public" do
test "indexes posts that are public or unlisted" 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"
})
Enum.each(["public", "unlisted"], fn visibility ->
{:ok, activity} =
CommonAPI.post(user, %{
status: "Well this is a story all about how my life got flipped turned upside down",
visibility: visibility
})
args = %{"op" => "add_to_index", "activity" => activity.id}
args = %{"op" => "add_to_index", "activity" => activity.id}
assert_enqueued(worker: SearchIndexingWorker, args: args)
assert_enqueued(worker: SearchIndexingWorker, args: args)
end)
end
test "doesn't index posts that are not public" do
@ -66,4 +72,83 @@ defmodule Pleroma.SearchTest do
refute_enqueued(worker: SearchIndexingWorker, args: args)
end
test "object_to_search_data includes content warnings and attachment descriptions" do
object = %Object{
id: 1,
data: %{
"id" => "https://example.com/objects/1",
"type" => "Note",
"content" => "body text",
"summary" => "subject text",
"attachment" => [
%{"type" => "Document", "name" => "image description"},
%{
"type" => "Document",
"name" => "remote-image.jpg",
"summary" => "remote image description"
},
%{"type" => "Document", "name" => ""}
],
"published" => DateTime.utc_now() |> DateTime.to_iso8601()
}
}
assert %{content: content} = Search.object_to_search_data(object)
assert content =~ "body text"
assert content =~ "subject text"
assert content =~ "image description"
assert content =~ "remote image description"
refute content =~ "remote-image.jpg"
end
test "object_to_search_data ignores objects with invalid published dates" do
for published <- [nil, 123, "not a date"] do
object = %Object{
id: 1,
data: %{
"id" => "https://example.com/objects/1",
"type" => "Note",
"content" => "body text",
"published" => published
}
}
refute Search.object_to_search_data(object)
end
end
test "indexable? accepts searchable content warnings and attachment descriptions" do
public = Pleroma.Constants.as_public()
published = DateTime.utc_now() |> DateTime.to_iso8601()
for data <- [
%{"content" => ".", "summary" => "subject text", "attachment" => []},
%{
"content" => ".",
"summary" => "",
"attachment" => [%{"type" => "Document", "name" => "image description"}]
}
] do
object = %Object{
id: 1,
data:
Map.merge(data, %{
"id" => "https://example.com/objects/1",
"type" => "Note",
"to" => [public],
"published" => published
})
}
activity = %Pleroma.Activity{
id: Ecto.UUID.generate(),
data: %{"type" => "Create"},
object: object,
recipients: [public]
}
assert Search.indexable?(activity)
end
end
end

View file

@ -11,12 +11,14 @@ defmodule Pleroma.UserTest do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Webhook.Notify
use Pleroma.DataCase, async: false
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
import ExUnit.CaptureLog
import Mock
import Swoosh.TestAssertions
setup do
@ -719,6 +721,14 @@ defmodule Pleroma.UserTest do
assert user.is_confirmed
end
test_with_mock "triggers webhooks", Notify, trigger_webhooks: fn _, _ -> nil end do
cng = User.register_changeset(%User{}, @full_user_data)
{:ok, registered_user} = User.register(cng)
assert_called(Notify.trigger_webhooks(registered_user, :"account.created"))
end
end
describe "user registration, with :account_activation_required" do

View file

@ -729,6 +729,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert Activity.get_by_ap_id(data["id"])
end
test "rejects duplicate Signature headers before processing signed inbox requests", %{
conn: conn
} do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
conn =
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> Map.update!(:req_headers, &[{"signature", "sig1=:fake-signature:"} | &1])
|> post("/inbox", data)
assert "error, unsupported HTTP Message Signature" == json_response(conn, 401)
assert [] == all_enqueued(worker: ReceiverWorker)
end
test "it inserts an incoming activity into the database" <>
"even if we can't fetch the user but have it in our db",
%{conn: conn} do
@ -757,6 +773,120 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert Activity.get_by_ap_id(data["id"])
end
test "rejects unsupported HTTP Message Signatures so clients can fall back", %{conn: conn} do
data = %{
"type" => "Create",
"actor" => "https://activitypubbot.example/user/ok",
"id" => "https://activitypubbot.example/activities/rfc9421-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "https://activitypubbot.example/objects/rfc9421-note",
"actor" => "https://activitypubbot.example/user/ok",
"attributedTo" => "https://activitypubbot.example/user/ok",
"content" => "hello from activitypub-bot",
"published" => "2026-05-29T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> false end)
conn =
conn
|> put_req_header("content-type", "application/activity+json")
|> put_req_header("date", "Fri, 29 May 2026 13:33:31 GMT")
|> put_req_header("content-digest", "sha-256=:fake-digest:")
|> put_req_header(
"signature-input",
"sig1=(\"@method\" \"@target-uri\" \"date\" \"content-type\" \"content-digest\");keyid=\"https://activitypubbot.example/user/ok/publickey\";alg=\"rsa-v1_5-sha256\";created=1780061611"
)
|> put_req_header("signature", "sig1=:fake-signature:")
|> post("/inbox", data)
assert "error, unsupported HTTP Message Signature" == json_response(conn, 401)
assert [] == all_enqueued(worker: SignatureRetryWorker)
end
test "rejects duplicate Signature headers so clients can fall back", %{conn: conn} do
data = %{
"type" => "Create",
"actor" => "https://activitypubbot.example/user/ok",
"id" => "https://activitypubbot.example/activities/duplicate-signature-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "https://activitypubbot.example/objects/duplicate-signature-note",
"actor" => "https://activitypubbot.example/user/ok",
"attributedTo" => "https://activitypubbot.example/user/ok",
"content" => "hello from activitypub-bot",
"published" => "2026-05-29T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
legacy_signature =
"keyId=\"https://activitypubbot.example/user/ok#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> false end)
conn =
conn
|> put_req_header("content-type", "application/activity+json")
|> put_req_header("date", "Fri, 29 May 2026 13:33:31 GMT")
|> put_req_header("digest", "SHA-256=fake-digest")
|> put_req_header("signature", "sig1=:fake-signature:")
|> Map.update!(:req_headers, &[{"signature", legacy_signature} | &1])
|> post("/inbox", data)
assert "error, unsupported HTTP Message Signature" == json_response(conn, 401)
assert [] == all_enqueued(worker: SignatureRetryWorker)
end
test "keeps failed-signature retry for legacy signatures with Signature-Input", %{conn: conn} do
data = %{
"type" => "Create",
"actor" => "https://activitypubbot.example/user/ok",
"id" => "https://activitypubbot.example/activities/legacy-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "https://activitypubbot.example/objects/legacy-note",
"actor" => "https://activitypubbot.example/user/ok",
"attributedTo" => "https://activitypubbot.example/user/ok",
"content" => "hello from activitypub-bot",
"published" => "2026-05-29T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> false end)
conn =
conn
|> put_req_header("content-type", "application/activity+json")
|> put_req_header("date", "Fri, 29 May 2026 13:33:31 GMT")
|> put_req_header("digest", "SHA-256=fake-digest")
|> put_req_header(
"signature-input",
"sig1=(\"@method\" \"@target-uri\" \"date\" \"content-type\" \"digest\");keyid=\"https://activitypubbot.example/user/ok/publickey\";alg=\"rsa-v1_5-sha256\";created=1780061611"
)
|> put_req_header(
"signature",
"keyId=\"https://activitypubbot.example/user/ok#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
)
|> post("/inbox", data)
assert "ok" == json_response(conn, 200)
assert [_] = all_enqueued(worker: SignatureRetryWorker)
end
test "does not create a forged post after failed signature retry", %{conn: conn} do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
@ -1320,6 +1450,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert Activity.get_by_ap_id(data["id"])
end
test "rejects duplicate Signature headers", %{conn: conn, data: data} do
user = insert(:user)
conn =
conn
|> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json")
|> Map.update!(:req_headers, &[{"signature", "sig1=:fake-signature:"} | &1])
|> post("/users/#{user.nickname}/inbox", data)
assert "error, unsupported HTTP Message Signature" == json_response(conn, 401)
assert [] == all_enqueued(worker: ReceiverWorker)
end
test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
user = insert(:user)

View file

@ -17,6 +17,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
alias Pleroma.Webhook.Notify
import ExUnit.CaptureLog
import Mock
@ -1877,6 +1878,29 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert Repo.aggregate(Object, :count, :id) == 1
assert Repo.aggregate(Notification, :count, :id) == 0
end
test_with_mock "triggers webhooks",
%{
reporter: reporter,
context: context,
target_account: target_account,
reported_activity: reported_activity,
content: content
},
Notify,
[:passthrough],
trigger_webhooks: fn _, _ -> nil end do
{:ok, activity} =
ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported_activity],
content: content
})
assert_called(Notify.trigger_webhooks(activity, :"report.created"))
end
end
test "fetch_activities/2 returns activities addressed to a list " do

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,72 @@
# 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.FedidevFunInReplyToTest 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_fixture!(rel_path) do
data = rel_path |> File.read!() |> Jason.decode!()
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
{activity, object}
end
test "normalizes Link inReplyTo to a string" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/in_reply_to_link.json")
assert object.data["inReplyTo"] == "https://fedidev.fun/assets/note1.jsonap"
end
test "normalizes list inReplyTo to the first object ID" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/in_reply_to_list.json")
assert object.data["inReplyTo"] == "https://fedidev.fun/assets/note1.jsonap"
end
test "drops invalid map shapes in inReplyTo" do
{_activity, object} =
ingest_fixture!("test/fixtures/fedidev.fun/in_reply_to_collection.json")
assert object.data["inReplyTo"] == nil
end
test "accepts embedded objects in inReplyTo" do
{_activity, object} =
ingest_fixture!("test/fixtures/fedidev.fun/in_reply_to_embedded_note.json")
assert object.data["inReplyTo"] == "https://fedidev.fun/assets/note1.jsonap"
end
end

View file

@ -0,0 +1,49 @@
# 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.FedidevFunMissingAttributedToTest 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
test "inherits attributedTo from surrounding activity" do
data = File.read!("test/fixtures/fedidev.fun/missing_attributed_to.json") |> Jason.decode!()
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
assert object.data["attributedTo"] == data["actor"]
assert object.data["actor"] == data["actor"]
end
end

View file

@ -0,0 +1,64 @@
# 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.FedidevFunObjectTypeListTest 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_fixture!(rel_path) do
data = rel_path |> File.read!() |> Jason.decode!()
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
{activity, object}
end
test "accepts object type as a list and stores the first type" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/object_type_list.json")
assert object.data["type"] == "Note"
end
test "selects a supported type after an extension type" do
data = File.read!("test/fixtures/fedidev.fun/object_type_list.json") |> Jason.decode!()
data = put_in(data, ["object", "type"], ["https://example.com/ns#CustomType", "Note"])
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
assert object.data["type"] == "Note"
end
end

View file

@ -0,0 +1,63 @@
# 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.FedidevFunQuoteUrlListTest 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_fixture!(rel_path) do
data = rel_path |> File.read!() |> Jason.decode!()
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
{activity, object}
end
test "normalizes quoteUri list to the first object ID" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/quote_uri_list.json")
assert object.data["quoteUrl"] == "https://fedidev.fun/assets/note1.jsonap"
end
test "normalizes quoteUrl list to the first object ID" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/quote_url_list.json")
assert object.data["quoteUrl"] == "https://fedidev.fun/assets/note1.jsonap"
end
test "normalizes _misskey_quote list to the first object ID" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/misskey_quote_list.json")
assert object.data["quoteUrl"] == "https://fedidev.fun/assets/note1.jsonap"
end
end

View file

@ -0,0 +1,112 @@
# 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.FedidevFunTagInferenceTest 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_fixture!(rel_path) do
data = rel_path |> File.read!() |> Jason.decode!()
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
{activity, object}
end
test "infers Hashtag type when missing" do
{_activity, object} =
ingest_fixture!("test/fixtures/fedidev.fun/tag_hashtag_missing_type.json")
assert "test" in Object.hashtags(object)
end
test "infers Mention type when missing" do
{_activity, object} =
ingest_fixture!("test/fixtures/fedidev.fun/tag_mention_missing_type.json")
assert Enum.any?(object.data["tag"], fn
%{"type" => "Mention", "href" => "https://remote.example/users/bob"} -> true
_ -> false
end)
end
test "normalizes a list-typed tag type" do
data =
File.read!("test/fixtures/fedidev.fun/tag_hashtag_missing_type.json") |> Jason.decode!()
data = put_in(data, ["object", "tag", Access.at(0), "type"], ["Hashtag"])
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
assert "test" in Object.hashtags(object)
end
test "ignores a list-typed Hashtag without a string name" do
data =
File.read!("test/fixtures/fedidev.fun/tag_hashtag_missing_type.json") |> Jason.decode!()
data = put_in(data, ["object", "tag", Access.at(0), "type"], ["Hashtag"])
data = put_in(data, ["object", "tag", Access.at(0), "name"], nil)
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
assert Object.hashtags(object) == []
end
test "infers a Link before deriving quoteUrl" do
data =
File.read!("test/fixtures/fedidev.fun/tag_hashtag_missing_type.json") |> Jason.decode!()
data =
put_in(data, ["object", "tag"], [
%{
"href" => "https://fedidev.fun/assets/note1.jsonap",
"mediaType" => "application/activity+json"
}
])
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
assert object.data["quoteUrl"] == "https://fedidev.fun/assets/note1.jsonap"
end
end

View file

@ -0,0 +1,53 @@
# 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.FedidevFunUrlListTest 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_fixture!(rel_path) do
data = rel_path |> File.read!() |> Jason.decode!()
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
{activity, object}
end
test "normalizes object url list to a string" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/url_list.json")
assert object.data["url"] == "https://fedidev.fun/objects/123"
end
end

View file

@ -572,11 +572,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
Map.put(data["object"], "inReplyTo", ["https://shitposter.club/notice/2827873"])
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == ["https://shitposter.club/notice/2827873"]
assert modified_object["inReplyTo"] == "https://shitposter.club/notice/2827873"
object_with_reply = Map.put(data["object"], "inReplyTo", [])
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == []
assert modified_object["inReplyTo"] == nil
end
test "returns modified object when allowed incoming reply", %{data: data} do
@ -671,13 +671,21 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
test "returns object with emoji when object contains list tags" do
assert Transmogrifier.fix_emoji(%{
"tag" => [
%{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}},
%{
"type" => "Emoji",
"name" => ":bib:",
"icon" => %{"url" => "https://example.com/test"}
},
%{"type" => "Hashtag"}
]
}) == %{
"emoji" => %{"bib" => "/test"},
"emoji" => %{"bib" => "https://example.com/test"},
"tag" => [
%{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"},
%{
"icon" => %{"url" => "https://example.com/test"},
"name" => ":bib:",
"type" => "Emoji"
},
%{"type" => "Hashtag"}
]
}
@ -685,10 +693,28 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
test "returns object with emoji when object contains map tag" do
assert Transmogrifier.fix_emoji(%{
"tag" => %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}}
"tag" => %{
"type" => "Emoji",
"name" => ":bib:",
"icon" => %{"url" => "https://example.com/test"}
}
}) == %{
"emoji" => %{"bib" => "/test"},
"tag" => %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"}
"emoji" => %{"bib" => "https://example.com/test"},
"tag" => %{
"icon" => %{"url" => "https://example.com/test"},
"name" => ":bib:",
"type" => "Emoji"
}
}
end
end
describe "fix_tag/1" do
test "ignores malformed Hashtag names and tag entries" do
tag = %{"type" => "Hashtag", "name" => 123}
assert Transmogrifier.fix_tag(%{"tag" => [tag, "malformed"]}) == %{
"tag" => [tag, "malformed"]
}
end
end

View file

@ -50,6 +50,25 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.UserUpdateHandlingTest do
assert user.bio == "<p>Some bio</p>"
end
test "it selects an actor type after an extension type" do
user = insert(:user, local: false)
update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
object =
update_data["object"]
|> Map.put("actor", user.ap_id)
|> Map.put("id", user.ap_id)
|> Map.put("type", ["https://example.com/ns#CustomActor", "Person"])
update_data =
update_data
|> Map.put("actor", user.ap_id)
|> Map.put("object", object)
assert {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(update_data)
assert User.get_cached_by_ap_id(user.ap_id).name == "gargle"
end
test "it works with alsoKnownAs" do
%{ap_id: actor} = insert(:user, local: false)

View file

@ -121,6 +121,19 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
end)
end
test "effective configuration omits static search settings", %{conn: conn} do
insert(:config, key: Pleroma.Search, value: [module: Pleroma.Search.ParadeDB])
insert(:config, key: Pleroma.Search.ParadeDB, value: [url: "postgres://example/db"])
%{"configs" => configs} =
conn
|> get("/api/pleroma/admin/config")
|> json_response_and_validate_schema(200)
refute Enum.any?(configs, &(&1["key"] == "Pleroma.Search"))
refute Enum.any?(configs, &(&1["key"] == "Pleroma.Search.ParadeDB"))
end
test "subkeys with full update right merge", %{conn: conn} do
insert(:config,
key: ":emoji",
@ -1552,12 +1565,15 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
assert web_endpoint["children"]
end
test "all keys from description are whitelisted", %{conn: conn} do
test "descriptions exclude blacklisted keys", %{conn: conn} do
conn = get(conn, "/api/pleroma/admin/config/descriptions")
assert response = json_response_and_validate_schema(conn, 200)
assert length(response) == length(Pleroma.Docs.JSON.compiled_descriptions())
refute Enum.any?(response, &(&1["key"] == "Pleroma.Search"))
refute Enum.any?(response, &(&1["key"] == "Pleroma.Search.ParadeDB"))
assert length(response) == length(Pleroma.Docs.JSON.compiled_descriptions()) - 2
end
test "filters by database configuration blacklist and whitelist", %{conn: conn} do

View file

@ -176,6 +176,183 @@ defmodule Pleroma.Web.AdminAPI.UserControllerTest do
assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == []
end
test "Admin-created users bypass account confirmation", %{conn: conn} do
clear_config([:instance, :account_activation_required], true)
conn
|> put_req_header("accept", "application/json")
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{"nickname" => "lain", "email" => "lain@example.org", "password" => "test"}
]
})
|> json_response_and_validate_schema(200)
assert User.get_by_nickname("lain").is_confirmed
end
test "Creating without a password returns a password reset link", %{conn: conn} do
accounts =
conn
|> put_req_header("accept", "application/json")
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => "lain",
"email" => "lain@example.org"
},
%{
"nickname" => "lain2",
"email" => "lain2@example.org",
"password" => "dupa.8"
}
]
})
|> json_response_and_validate_schema(200)
|> Map.new(fn account -> {account["data"]["nickname"], account} end)
account1 = accounts["lain"]
account2 = accounts["lain2"]
assert account1["type"] == "success"
assert %{"password_reset_link" => link} = account1["data"]
assert link =~ "/api/v1/pleroma/password_reset/"
assert account2["type"] == "success"
refute Map.has_key?(account2["data"], "password_reset_link")
end
test "Explicit empty passwords return a conflict", %{conn: conn} do
conn =
conn
|> put_req_header("accept", "application/json")
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{"nickname" => "lain", "email" => "lain@example.org", "password" => ""}
]
})
assert [%{"error" => "password can't be blank"}] = json_response(conn, 409)
refute User.get_by_nickname("lain")
end
test "Password reset token failures roll back the user batch", %{conn: conn} do
idempotency_key = Ecto.UUID.generate()
request = fn conn ->
conn
|> put_req_header("accept", "application/json")
|> put_req_header("content-type", "application/json")
|> put_req_header("idempotency-key", idempotency_key)
|> post("/api/pleroma/admin/users", %{
"users" => [
%{"nickname" => "lain", "email" => "lain@example.org"},
%{"nickname" => "lain2", "email" => "lain2@example.org"}
]
})
end
with_mock Pleroma.PasswordResetToken, [:passthrough],
create_token: fn user ->
if user.nickname == "lain2" do
{:error, :forced}
else
result = passthrough([user])
send(self(), {:token_created, user.nickname})
result
end
end do
conn = request.(conn)
assert json_response(conn, 500) == %{"error" => "Something went wrong"}
end
assert_received {:token_created, "lain"}
refute User.get_by_nickname("lain")
refute User.get_by_nickname("lain2")
assert Repo.aggregate(Pleroma.PasswordResetToken, :count) == 0
assert [_account1, _account2] = conn |> request.() |> json_response(200)
end
test "Password reset links are not cached by idempotency key", %{conn: conn} do
idempotency_key = Ecto.UUID.generate()
request = fn conn ->
conn
|> put_req_header("accept", "application/json")
|> put_req_header("content-type", "application/json")
|> put_req_header("idempotency-key", idempotency_key)
|> post("/api/pleroma/admin/users", %{
"users" => [%{"nickname" => "lain", "email" => "lain@example.org"}]
})
end
assert [%{"data" => %{"password_reset_link" => _link}}] =
conn |> request.() |> json_response(200)
second_conn = request.(conn)
assert [_error] = json_response(second_conn, 409)
refute get_resp_header(second_conn, "idempotent-replayed") == ["true"]
end
test "Explicit-password responses remain idempotent", %{conn: conn} do
idempotency_key = Ecto.UUID.generate()
request = fn conn ->
conn
|> put_req_header("accept", "application/json")
|> put_req_header("content-type", "application/json")
|> put_req_header("idempotency-key", idempotency_key)
|> post("/api/pleroma/admin/users", %{
"users" => [
%{"nickname" => "lain", "email" => "lain@example.org", "password" => "test"}
]
})
end
assert [_account] = conn |> request.() |> json_response(200)
second_conn = request.(conn)
assert [_account] = json_response(second_conn, 200)
assert get_resp_header(second_conn, "idempotent-replayed") == ["true"]
end
test "Duplicate nicknames in a batch return a conflict", %{conn: conn} do
conn =
conn
|> put_req_header("accept", "application/json")
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{"nickname" => "lain", "email" => "lain1@example.org", "password" => "test"},
%{"nickname" => "lain", "email" => "lain2@example.org", "password" => "test"}
]
})
assert [_first, _second] = json_response(conn, 409)
refute User.get_by_nickname("lain")
end
test "Blank nicknames return a conflict", %{conn: conn} do
conn =
conn
|> put_req_header("accept", "application/json")
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{"nickname" => "", "email" => "lain@example.org", "password" => "test"}
]
})
assert [%{"error" => "nickname can't be blank"}] = json_response(conn, 409)
end
test "Cannot create user with existing email", %{conn: conn} do
user = insert(:user)

View file

@ -0,0 +1,156 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.WebhookControllerTest do
use Pleroma.Web.ConnCase, async: false
import Pleroma.Factory
alias Pleroma.Webhook
setup do
admin = insert(:user, is_admin: true)
token = insert(:oauth_admin_token, user: admin)
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
{:ok, %{admin: admin, token: token, conn: conn}}
end
describe "GET /api/pleroma/admin/webhook" do
test "lists existing webhooks", %{conn: conn} do
Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]})
Webhook.create(%{url: "https://example.com/webhook2", events: [:"account.created"]})
response =
conn
|> get("/api/pleroma/admin/webhooks")
|> json_response_and_validate_schema(:ok)
assert length(response) == 2
end
test "requires an admin even if a moderator can manage announcements" do
clear_config([:instance, :moderator_privileges], [:announcements_manage_announcements])
moderator = insert(:user, is_moderator: true)
token = insert(:oauth_admin_token, user: moderator)
build_conn()
|> assign(:user, moderator)
|> assign(:token, token)
|> get("/api/pleroma/admin/webhooks")
|> json_response(:forbidden)
end
end
describe "POST /api/pleroma/admin/webhooks" do
test "creates a webhook", %{conn: conn} do
%{"id" => id} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/webhooks", %{
url: "http://example.com/webhook",
events: ["account.created"]
})
|> json_response_and_validate_schema(:ok)
assert %{url: "http://example.com/webhook", events: [:"account.created"]} = Webhook.get(id)
end
test "rejects empty events", %{conn: conn} do
conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/webhooks", %{
url: "http://example.com/webhook",
events: []
})
|> json_response(:bad_request)
end
test "rejects internal flag", %{conn: conn} do
conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/webhooks", %{
url: "http://example.com/webhook",
events: ["account.created"],
internal: true
})
|> json_response(:bad_request)
end
end
describe "PATCH /api/pleroma/admin/webhooks" do
test "edits a webhook", %{conn: conn} do
{:ok, %{id: id}} =
Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]})
conn
|> put_req_header("content-type", "application/json")
|> patch("/api/pleroma/admin/webhooks/#{id}", %{
events: ["report.created", "account.created"]
})
|> json_response_and_validate_schema(:ok)
assert %{events: [:"report.created", :"account.created"]} = Webhook.get(id)
end
test "rejects empty events", %{conn: conn} do
{:ok, %{id: id}} =
Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]})
conn
|> put_req_header("content-type", "application/json")
|> patch("/api/pleroma/admin/webhooks/#{id}", %{events: []})
|> json_response(:bad_request)
assert %{events: [:"report.created"]} = Webhook.get(id)
end
test "rejects internal flag", %{conn: conn} do
{:ok, %{id: id}} =
Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]})
conn
|> put_req_header("content-type", "application/json")
|> patch("/api/pleroma/admin/webhooks/#{id}", %{internal: true})
|> json_response(:bad_request)
assert %{internal: false} = Webhook.get(id)
end
test "can't edit an internal webhook", %{conn: conn} do
{:ok, %{id: id}} =
Webhook.create(%{url: "https://example.com/webhook1", events: [], internal: true})
conn
|> put_req_header("content-type", "application/json")
|> patch("/api/pleroma/admin/webhooks/#{id}", %{
events: ["report.created", "account.created"]
})
|> json_response_and_validate_schema(:forbidden)
assert %{events: []} = Webhook.get(id)
end
end
describe "DELETE /api/pleroma/admin/webhooks" do
test "deletes a webhook", %{conn: conn} do
{:ok, %{id: id}} =
Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]})
conn
|> put_req_header("content-type", "application/json")
|> delete("/api/pleroma/admin/webhooks/#{id}")
|> json_response_and_validate_schema(:ok)
assert [] =
Webhook
|> Pleroma.Repo.all()
end
end
end

View file

@ -0,0 +1,327 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.Admin.AccountControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do
admin = insert(:user, is_admin: true)
token = insert(:oauth_admin_token, user: admin)
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
{:ok, %{admin: admin, token: token, conn: conn}}
end
describe "GET /api/v1/admin/accounts" do
test "search by display name", %{conn: conn} do
%{id: id} = insert(:user, name: "Display name")
insert(:user, name: "Other name")
assert [%{"id" => ^id}] =
conn
|> get("/api/v1/admin/accounts?display_name=Display")
|> json_response_and_validate_schema(200)
end
end
# Adapted from
# https://github.com/mastodon/mastodon/blob/main/spec/requests/api/v2/admin/accounts_spec.rb
describe "GET /api/v2/admin/accounts" do
setup do
remote_account = insert(:user, nickname: "remote@example.org", local: false)
other_remote_account = insert(:user, nickname: "other@foo.bar", local: false)
# suspended_account = insert(:user)
# suspended_remote = insert(:user)
disabled_account = insert(:user, is_active: false)
pending_account = insert(:user, is_approved: false)
admin_account = insert(:user, is_admin: true)
{:ok,
%{
remote_account: remote_account,
other_remote_account: other_remote_account,
disabled_account: disabled_account,
pending_account: pending_account,
admin_account: admin_account
}}
end
# test "returns the correct accounts when called with status active
# and origin local and permissions staff", %{
# conn: conn,
# admin_account: %{id: admin_account_id}
# } do
# assert [%{"id" => ^admin_account_id}] =
# conn
# |> get("/api/v2/admin/accounts?status=active&origin=local&permissions=staff")
# |> json_response_and_validate_schema(200)
# end
test "returns the correct accounts when called with by_domain value and origin remote", %{
conn: conn,
remote_account: %{id: remote_account_id}
} do
assert [%{"id" => ^remote_account_id}] =
conn
|> get("/api/v2/admin/accounts?by_domain=example.org&origin=remote")
|> json_response_and_validate_schema(200)
end
# test "returns the correct accounts when called with status suspended", %{
# conn: conn,
# suspended_account: %{id: suspended_account_id}
# } do
# assert [%{"id" => ^suspended_account_id}] =
# conn
# |> get("/api/v2/admin/accounts?status=suspended")
# |> json_response_and_validate_schema(200)
# end
test "returns the correct accounts when called with status disabled", %{
conn: conn,
disabled_account: %{id: disabled_account_id}
} do
assert [%{"id" => ^disabled_account_id}] =
conn
|> get("/api/v2/admin/accounts?status=disabled")
|> json_response_and_validate_schema(200)
end
test "returns the correct accounts when called with status pending", %{
conn: conn,
pending_account: %{id: pending_account_id}
} do
assert [%{"id" => ^pending_account_id}] =
conn
|> get("/api/v2/admin/accounts?status=pending")
|> json_response_and_validate_schema(200)
end
test "sets the correct pagination headers with limit param", %{
conn: conn,
admin_account: %{id: admin_account_id}
} do
response =
conn
|> get("/api/v2/admin/accounts?limit=1")
next_url =
~r{<.+?(?<link>/api[^>]+)>; rel=\"next\"}
|> Regex.named_captures(get_resp_header(response, "link") |> Enum.at(0))
|> Map.get("link")
next_url =~ "&limit=1&max_id=#{admin_account_id}"
end
end
describe "GET /api/v1/admin/accounts/:id" do
test "show admin-level information", %{conn: conn} do
%{id: id} =
insert(:user,
email: "email@example.com",
is_confirmed: false,
is_moderator: true
)
assert %{
"id" => ^id,
"email" => "email@example.com",
"confirmed" => false,
"role" => "moderator"
} =
conn
|> get("/api/v1/admin/accounts/#{id}")
|> json_response_and_validate_schema(200)
end
end
describe "DELETE /api/v1/admin/accounts/:id" do
test "delete account", %{conn: conn} do
%{id: id} = user = insert(:user)
conn
|> delete("/api/v1/admin/accounts/#{id}")
|> json_response_and_validate_schema(200)
user = Repo.reload!(user)
assert %{is_active: false} = user
end
end
describe "POST /api/v1/admin/accounts/:id/action" do
test "disable account", %{conn: conn} do
%{id: id} = user = insert(:user)
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/admin/accounts/#{id}/action", %{
"type" => "disable"
})
|> json_response_and_validate_schema(204)
user = Repo.reload!(user)
assert %{is_active: false} = user
end
test "does not disable a remote account", %{conn: conn} do
%{id: id} = user = insert(:user, local: false)
assert %{"error" => "Only local accounts can be disabled"} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/admin/accounts/#{id}/action", %{
"type" => "disable"
})
|> json_response_and_validate_schema(400)
assert %{is_active: true} = Repo.reload!(user)
end
test "perform action with assigned report", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
{:ok, %{id: report_id} = report} =
CommonAPI.report(reporter, %{
account_id: target_user.id
})
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/admin/accounts/#{target_user.id}/action", %{
"type" => "none",
"report_id" => report_id
})
|> json_response_and_validate_schema(204)
report = Repo.reload!(report)
assert %{data: %{"state" => "resolved"}} = report
end
test "requires the report management privilege", %{conn: conn} do
clear_config([:instance, :admin_privileges], [:users_manage_activation_state])
%{id: id} = insert(:user)
assert %{"error" => _} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/admin/accounts/#{id}/action", %{"type" => "none"})
|> json_response(403)
end
test "does not resolve a report for another account", %{conn: conn} do
[reporter, target_user, other_user] = insert_list(3, :user)
{:ok, report} =
CommonAPI.report(reporter, %{
account_id: target_user.id
})
assert %{"error" => _} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/admin/accounts/#{other_user.id}/action", %{
"type" => "none",
"report_id" => report.id
})
|> json_response_and_validate_schema(400)
assert %{data: %{"state" => "open"}} = Repo.reload!(report)
end
test "requires an action type", %{conn: conn} do
%{id: id} = insert(:user)
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/admin/accounts/#{id}/action", %{})
|> json_response_and_validate_schema(400)
end
test "rejects unsupported action types", %{conn: conn} do
%{id: id} = insert(:user)
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/admin/accounts/#{id}/action", %{"type" => "sensitive"})
|> json_response_and_validate_schema(400)
end
end
describe "POST /api/v1/admin/accounts/:id/enable" do
test "enable account", %{conn: conn} do
%{id: id} = user = insert(:user)
User.set_activation(user, false)
conn
|> post("/api/v1/admin/accounts/#{id}/enable")
|> json_response_and_validate_schema(200)
user = Repo.reload!(user)
assert %{is_active: true} = user
end
end
describe "POST /api/v1/admin/accounts/:id/approve" do
test "approve account", %{conn: conn} do
%{id: id} = user = insert(:user, is_approved: false)
conn
|> post("/api/v1/admin/accounts/#{id}/approve")
|> json_response_and_validate_schema(200)
user = Repo.reload!(user)
assert %{is_approved: true} = user
end
end
describe "POST /api/v1/admin/accounts/:id/reject" do
test "reject account", %{conn: conn} do
%{id: id} = user = insert(:user, is_approved: false)
conn
|> post("/api/v1/admin/accounts/#{id}/reject")
|> json_response_and_validate_schema(200)
user = Repo.reload!(user)
assert %{is_active: false} = user
end
test "do not allow rejecting already accepted accounts", %{conn: conn} do
%{id: id} = user = insert(:user, is_approved: true)
assert %{"error" => "User is approved"} ==
conn
|> post("/api/v1/admin/accounts/#{id}/reject")
|> json_response_and_validate_schema(400)
user = Repo.reload!(user)
assert %{is_approved: true} = user
end
end
end

View file

@ -0,0 +1,239 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.Admin.ReportControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
alias Pleroma.Activity
alias Pleroma.Repo
alias Pleroma.Rule
alias Pleroma.Web.CommonAPI
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do
admin = insert(:user, is_admin: true)
token = insert(:oauth_admin_token, user: admin)
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
{:ok, %{admin: admin, token: token, conn: conn}}
end
describe "GET /api/v1/admin/reports" do
test "get reports by state", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id1}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "this user uses an app called Hitler Tusky",
status_ids: [activity.id]
})
{:ok, %{id: report_id2}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
status_ids: [activity.id]
})
CommonAPI.update_report_state(report_id2, "resolved")
assert [%{"id" => ^report_id1}] =
conn
|> get("/api/v1/admin/reports?unresolved=true")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^report_id1}] =
conn
|> get("/api/v1/admin/reports")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^report_id1}] =
conn
|> get("/api/v1/admin/reports?resolved=false")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^report_id2}, %{"id" => ^report_id1}] =
conn
|> get("/api/v1/admin/reports?resolved=true&unresolved=true")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^report_id2}] =
conn
|> get("/api/v1/admin/reports?resolved=true")
|> json_response_and_validate_schema(200)
end
test "get reports by target account", %{conn: conn} do
[reporter, target_user, other_target] = insert_list(3, :user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id
})
{:ok, _report} =
CommonAPI.report(reporter, %{
account_id: other_target.id
})
assert [%{"id" => ^report_id}] =
conn
|> get("/api/v1/admin/reports?target_account_id=#{target_user.id}")
|> json_response_and_validate_schema(200)
end
end
describe "GET /api/v1/admin/reports/:id" do
test "get report by id", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
status_ids: [activity.id]
})
assert %{"id" => ^report_id} =
conn
|> get("/api/v1/admin/reports/#{report_id}")
|> json_response_and_validate_schema(200)
end
test "returns report rules", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
rule = Rule.create(%{text: "Be excellent to each other"})
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
rule_ids: [rule.id]
})
assert %{
"rules" => [
%{"id" => rule_id, "text" => "Be excellent to each other"}
]
} =
conn
|> get("/api/v1/admin/reports/#{report_id}")
|> json_response_and_validate_schema(200)
assert rule_id == to_string(rule.id)
end
test "returns 404 for a missing report", %{conn: conn} do
conn
|> get("/api/v1/admin/reports/0")
|> json_response_and_validate_schema(404)
end
end
describe "POST /api/v1/admin/reports/:id/resolve" do
test "resolve a report", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
status_ids: [activity.id]
})
assert %{"id" => ^report_id, "action_taken" => true} =
conn
|> post("/api/v1/admin/reports/#{report_id}/resolve")
|> json_response_and_validate_schema(200)
end
test "does not mutate a non-report activity", %{conn: conn} do
activity = insert(:note_activity)
conn
|> post("/api/v1/admin/reports/#{activity.id}/resolve")
|> json_response_and_validate_schema(404)
refute Repo.reload!(activity).data["state"]
end
end
describe "POST /api/v1/admin/reports/:id/reopen" do
test "reopen a report", %{conn: conn} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
status_ids: [activity.id]
})
CommonAPI.update_report_state(report_id, "resolved")
assert %{"id" => ^report_id, "action_taken" => false} =
conn
|> post("/api/v1/admin/reports/#{report_id}/reopen")
|> json_response_and_validate_schema(200)
end
test "does not mutate a non-report activity", %{conn: conn} do
activity = insert(:note_activity)
conn
|> post("/api/v1/admin/reports/#{activity.id}/reopen")
|> json_response_and_validate_schema(404)
refute Repo.reload!(activity).data["state"]
end
end
describe "POST /api/v1/admin/reports/:id/assign_to_self" do
test "assign and unassign a report", %{admin: admin, conn: conn} do
[reporter, target_user] = insert_pair(:user)
{:ok, report} =
CommonAPI.report(reporter, %{
account_id: target_user.id
})
assert %{"assigned_account" => %{"id" => admin_id}} =
conn
|> post("/api/v1/admin/reports/#{report.id}/assign_to_self")
|> json_response_and_validate_schema(200)
assert admin_id == admin.id
assert Repo.reload!(report).data["assigned_account"] == admin.id
assert %{"assigned_account" => nil} =
conn
|> post("/api/v1/admin/reports/#{report.id}/unassign")
|> json_response_and_validate_schema(200)
refute Repo.reload!(report).data["assigned_account"]
end
test "does not assign a non-report activity", %{conn: conn} do
%Activity{id: id} = activity = insert(:note_activity)
conn
|> post("/api/v1/admin/reports/#{id}/assign_to_self")
|> json_response_and_validate_schema(404)
refute Repo.reload!(activity).data["assigned_account"]
end
end
end

View file

@ -458,6 +458,170 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
assert [%{"id" => ^image_post_id}] = json_response_and_validate_schema(conn, 200)
end
test "media respects activity restrictions when unauthenticated profiles are enabled", %{
conn: conn
} do
clear_config([:instance, :public], false)
clear_config([:restrict_unauthenticated, :profiles, :local], false)
user = insert(:user)
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, %{id: media_id}} = ActivityPub.upload(file, actor: user.ap_id)
{:ok, activity} = CommonAPI.post(user, %{status: "cofe", media_ids: [media_id]})
user_id = user.id
assert %{"id" => ^user_id} =
build_conn()
|> get("/api/v1/accounts/#{user.id}")
|> json_response_and_validate_schema(200)
assert [] =
build_conn()
|> get("/api/v1/accounts/#{user.id}/statuses?only_media=true")
|> json_response_and_validate_schema(200)
assert [%{"id" => activity_id}] =
conn
|> get("/api/v1/accounts/#{user.id}/statuses?only_media=true")
|> json_response_and_validate_schema(200)
assert activity_id == activity.id
end
test "remote activity restrictions apply to created and boosted objects", %{conn: conn} do
clear_config([:restrict_unauthenticated, :activities, :local], false)
clear_config([:restrict_unauthenticated, :activities, :remote], true)
local_user = insert(:user)
remote_user = insert(:user, local: false, domain: "example.com")
local_activity = insert(:note_activity, user: local_user)
mixed_create =
insert(:note_activity, local: true, object_local: false, user: local_user)
remote_activity =
insert(:note_activity, local: false, object_local: false, user: remote_user)
{:ok, announce} = CommonAPI.repeat(remote_activity.id, local_user)
status_ids =
build_conn()
|> get("/api/v1/accounts/#{local_user.id}/statuses")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["id"])
assert local_activity.id in status_ids
refute mixed_create.id in status_ids
refute announce.id in status_ids
authenticated_status_ids =
conn
|> get("/api/v1/accounts/#{local_user.id}/statuses")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["id"])
assert local_activity.id in authenticated_status_ids
assert mixed_create.id in authenticated_status_ids
assert announce.id in authenticated_status_ids
end
test "local activity restrictions apply to created and boosted objects", %{conn: conn} do
clear_config([:restrict_unauthenticated, :activities, :local], true)
clear_config([:restrict_unauthenticated, :activities, :remote], false)
local_user = insert(:user)
remote_user = insert(:user, local: false, domain: "example.com")
local_activity = insert(:note_activity, user: local_user)
remote_activity =
insert(:note_activity, local: false, object_local: false, user: remote_user)
mixed_create =
insert(:note_activity, local: false, object_local: true, user: remote_user)
remote_announce =
insert(:announce_activity, note_activity: local_activity, user: remote_user)
|> Ecto.Changeset.change(%{
local: false,
recipients: ["https://www.w3.org/ns/activitystreams#Public"]
})
|> Repo.update!()
status_ids =
build_conn()
|> get("/api/v1/accounts/#{remote_user.id}/statuses")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["id"])
assert remote_activity.id in status_ids
refute mixed_create.id in status_ids
refute remote_announce.id in status_ids
authenticated_status_ids =
conn
|> get("/api/v1/accounts/#{remote_user.id}/statuses")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["id"])
assert remote_activity.id in authenticated_status_ids
assert mixed_create.id in authenticated_status_ids
assert remote_announce.id in authenticated_status_ids
end
test "restricted local accounts reject activities with mismatched remote origins", %{
conn: conn
} do
clear_config([:restrict_unauthenticated, :activities, :local], true)
clear_config([:restrict_unauthenticated, :activities, :remote], false)
local_user = insert(:user)
mismatched_activity =
insert(:note_activity, local: false, object_local: false, user: local_user)
assert [] =
build_conn()
|> get("/api/v1/accounts/#{local_user.id}/statuses")
|> json_response_and_validate_schema(200)
assert [%{"id" => activity_id}] =
conn
|> get("/api/v1/accounts/#{local_user.id}/statuses")
|> json_response_and_validate_schema(200)
assert activity_id == mismatched_activity.id
end
test "restricted remote accounts reject activities with mismatched local origins", %{
conn: conn
} do
clear_config([:restrict_unauthenticated, :activities, :local], false)
clear_config([:restrict_unauthenticated, :activities, :remote], true)
remote_user = insert(:user, local: false, domain: "example.com")
mismatched_activity =
insert(:note_activity, local: true, object_local: true, user: remote_user)
assert [] =
build_conn()
|> get("/api/v1/accounts/#{remote_user.id}/statuses")
|> json_response_and_validate_schema(200)
assert [%{"id" => activity_id}] =
conn
|> get("/api/v1/accounts/#{remote_user.id}/statuses")
|> json_response_and_validate_schema(200)
assert activity_id == mismatched_activity.id
end
test "gets a user's statuses without reblogs", %{user: user, conn: conn} do
{:ok, %{id: post_id}} = CommonAPI.post(user, %{status: "HI!!!"})
{:ok, _} = CommonAPI.repeat(post_id, user)

View file

@ -2428,6 +2428,118 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
end
end
describe "cards" do
setup do
clear_config([:rich_media, :enabled], true)
Mox.stub_with(Pleroma.CachexMock, Pleroma.NullCache)
Pleroma.StaticStubbedConfigMock
|> stub(:get, fn
[:rich_media, :enabled] -> true
path -> Pleroma.Test.StaticConfig.get(path)
end)
oauth_access(["read:statuses"])
end
test "returns rich-media card", %{conn: conn, user: user} do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
{:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp"})
ObanHelpers.perform_all()
card_data = %{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"provider_name" => "example.com",
"provider_url" => "https://example.com",
"title" => "The Rock",
"type" => "link",
"url" => "https://example.com/ogp",
"description" =>
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
"pleroma" => %{
"opengraph" => %{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"title" => "The Rock",
"type" => "video.movie",
"url" => "https://example.com/ogp",
"description" =>
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer."
}
},
"author_name" => "",
"author_url" => "",
"authors" => [],
"blurhash" => nil,
"embed_url" => "",
"height" => 0,
"html" => "",
"image_description" => "",
"language" => nil,
"published_at" => nil,
"width" => 0
}
response =
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(200)
assert response["card"] == card_data
# works with private posts
{:ok, activity} =
CommonAPI.post(user, %{status: "https://example.com/ogp", visibility: "direct"})
response_two =
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(200)
assert response_two["card"] == card_data
end
test "replaces missing description with an empty string", %{conn: conn, user: user} do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
{:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp-missing-data"})
ObanHelpers.perform_all()
response =
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(:ok)
assert response["card"] == %{
"type" => "link",
"title" => "Pleroma",
"description" => "",
"image" => nil,
"provider_name" => "example.com",
"provider_url" => "https://example.com",
"url" => "https://example.com/ogp-missing-data",
"pleroma" => %{
"opengraph" => %{
"title" => "Pleroma",
"type" => "website",
"url" => "https://example.com/ogp-missing-data"
}
},
"author_name" => "",
"author_url" => "",
"authors" => [],
"blurhash" => nil,
"embed_url" => "",
"height" => 0,
"html" => "",
"image_description" => "",
"language" => nil,
"published_at" => nil,
"width" => 0
}
end
end
test "bookmarks" do
bookmarks_uri = "/api/v1/bookmarks"

View file

@ -545,11 +545,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
test "represent a relationship for the blocking and blocked user with expiry" do
user = insert(:user)
other_user = insert(:user)
date = DateTime.utc_now() |> DateTime.add(24 * 60 * 60) |> DateTime.truncate(:second)
{:ok, user, other_user} = User.follow(user, other_user)
{:ok, _subscription} = User.subscribe(user, other_user)
{:ok, _user_relationship} = User.block(user, other_user, %{duration: 24 * 60 * 60})
{:ok, user_relationship} = User.block(user, other_user, %{duration: 24 * 60 * 60})
{:ok, _user_relationship} = User.block(other_user, user)
expected =
@ -558,7 +556,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
%{
following: false,
blocking: true,
block_expires_at: date,
block_expires_at: user_relationship.expires_at,
blocked_by: true,
id: to_string(other_user.id)
}
@ -570,17 +568,19 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
test "represent a relationship for the muting user with expiry" do
user = insert(:user)
other_user = insert(:user)
date = DateTime.utc_now() |> DateTime.add(24 * 60 * 60) |> DateTime.truncate(:second)
{:ok, _user_relationship} =
{:ok, user_relationships} =
User.mute(user, other_user, %{notifications: true, duration: 24 * 60 * 60})
user_relationship =
Enum.find(user_relationships, &(&1.relationship_type == :mute))
expected =
Map.merge(
@blank_response,
%{
muting: true,
mute_expires_at: date,
mute_expires_at: user_relationship.expires_at,
muting_notifications: true,
id: to_string(other_user.id)
}

View file

@ -65,6 +65,15 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
}
test_notifications_rendering([notification], recipient, [expected])
grouped =
NotificationView.render("grouped_index.json", %{
notifications: [notification],
for: recipient
})
assert [group] = grouped.notification_groups
assert group.chat_message == expected.chat_message
end
test "Mention notification" do
@ -182,6 +191,16 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
}
test_notifications_rendering([notification], follower, [expected])
grouped =
NotificationView.render("grouped_index.json", %{
notifications: [notification],
for: follower
})
assert [group] = grouped.notification_groups
assert group.target_id == new_user.id
assert Enum.any?(grouped.accounts, &(&1.id == new_user.id))
end
test "EmojiReact notification" do
@ -210,6 +229,13 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
}
test_notifications_rendering([notification], user, [expected])
grouped =
NotificationView.render("grouped_index.json", %{notifications: [notification], for: user})
assert [group] = grouped.notification_groups
assert group.emoji == expected.emoji
assert group.emoji_url == expected.emoji_url
end
test "EmojiReact custom emoji notification" do
@ -295,6 +321,15 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
}
test_notifications_rendering([notification], moderator_user, [expected])
grouped =
NotificationView.render("grouped_index.json", %{
notifications: [notification],
for: moderator_user
})
assert [group] = grouped.notification_groups
assert group.report == expected.report
end
test "Edit notification" do

View file

@ -668,23 +668,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
assert expected == StatusView.render("attachment.json", %{attachment: object})
assert_schema(expected, "Attachment", api_spec)
end
test "uses the ActivityStreams image type when the MIME type is generic" do
object = %{
"type" => "Image",
"url" => [
%{
"mediaType" => "application/octet-stream",
"href" => "https://example.com/extensionless"
}
]
}
attachment = StatusView.render("attachment.json", %{attachment: object})
assert attachment.type == "image"
assert attachment.pleroma.mime_type == "application/octet-stream"
end
end
test "put the url advertised in the Activity in to the url attribute" do
@ -845,15 +828,31 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
fields = %{
"url" => page_url,
"site_name" => "Example site name",
"type" => "photo",
"title" => "Example website",
"image" => page_url <> "/example.jpg",
"description" => "Example description"
"description" => "Example description",
"author_name" => "Example author",
"author_url" => page_url <> "/author",
"provider_name" => "Example provider",
"provider_url" => page_url <> "/provider",
"html" => "<img src=\"#{page_url}/example.jpg\">",
"width" => "1024",
"height" => 768
}
{:ok, card} = Card.create(page_url, fields)
assert match?(%{provider_name: "example.com"}, StatusView.render("card.json", card))
assert %{
type: "photo",
author_name: "Example author",
author_url: "https://example.com/author",
provider_name: "Example provider",
provider_url: "https://example.com/provider",
html: "<img src=\"https://example.com/example.jpg\">",
width: 1024,
height: 768
} = StatusView.render("card.json", card)
end
test "a rich media card has all media proxied" do

View file

@ -71,10 +71,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
test "it performs ReverseProxy.call with valid signature", %{conn: conn, url: url} do
with_mock Pleroma.ReverseProxy,
call: fn _conn, _url, opts ->
assert opts[:sniff_content_type]
%Conn{status: :success}
end do
call: fn _conn, _url, _opts -> %Conn{status: :success} end do
assert %Conn{status: :success} = get(conn, url)
end
end

View file

@ -5,7 +5,11 @@
defmodule Pleroma.Web.OAuth.AppTest do
use Pleroma.DataCase, async: true
alias Pleroma.MFA.Token, as: MFAToken
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Push.Subscription
import Pleroma.Factory
describe "get_or_make/2" do
@ -57,29 +61,32 @@ defmodule Pleroma.Web.OAuth.AppTest do
test "removes orphaned apps" do
attrs = %{client_name: "Mastodon-Local", redirect_uris: "."}
{:ok, %App{} = old_app} = App.get_or_make(attrs, ["write"])
authorization = insert(:oauth_authorization, app: old_app)
token = insert(:oauth_token, app: old_app)
mfa_token = insert(:mfa_token, authorization: authorization)
push_subscription = insert(:push_subscription, token: token)
# backdate the old app so it's within the threshold for being cleaned up
one_hour_ago = DateTime.add(DateTime.utc_now(), -3600)
user = insert(:user)
attrs = %{client_name: "OldButValid", redirect_uris: ".", user_id: user.id}
{:ok, %App{} = old_owned_app} = App.get_or_make(attrs, ["write"])
one_hour_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -3600)
{:ok, _} =
"UPDATE apps SET inserted_at = $1, updated_at = $1 WHERE id = $2"
|> Pleroma.Repo.query([one_hour_ago, old_app.id])
"UPDATE apps SET inserted_at = $1, updated_at = $1 WHERE id IN ($2, $3)"
|> Repo.query([one_hour_ago, old_app.id, old_owned_app.id])
# Create the new app after backdating the old one
attrs = %{client_name: "PleromaFE", redirect_uris: "."}
{:ok, %App{} = app} = App.get_or_make(attrs, ["write"])
# Ensure the new app has a recent timestamp
now = DateTime.utc_now()
{:ok, _} =
"UPDATE apps SET inserted_at = $1, updated_at = $1 WHERE id = $2"
|> Pleroma.Repo.query([now, app.id])
{:ok, %App{} = recent_app} = App.get_or_make(attrs, ["write"])
App.remove_orphans()
assert [returned_app] = Pleroma.Repo.all(App)
assert returned_app.client_name == "PleromaFE"
assert returned_app.id == app.id
refute Repo.get(App, old_app.id)
refute Repo.get(Authorization, authorization.id)
refute Repo.get(Token, token.id)
refute Repo.get(MFAToken, mfa_token.id)
refute Repo.get(Subscription, push_subscription.id)
assert Repo.get(App, recent_app.id)
assert Repo.get(App, old_owned_app.id)
end
end

View file

@ -70,6 +70,139 @@ defmodule Pleroma.Web.Plugs.IdempotencyPlugTest do
assert [^key] = Conn.get_resp_header(conn, "idempotency-key")
end
test "does not cache responses marked as sensitive" do
key = Ecto.UUID.generate()
:post
|> conn("/cofe")
|> put_req_header("idempotency-key", key)
|> Conn.put_resp_header("x-request-id", "sensitive-request")
|> Conn.put_resp_content_type("application/json")
|> IdempotencyPlug.call([])
|> Conn.put_private(:skip_idempotency_cache, true)
|> Conn.send_resp(200, "secret")
conn =
:post
|> conn("/cofe")
|> put_req_header("idempotency-key", key)
|> Conn.put_resp_header("x-request-id", "second-request")
|> Conn.put_resp_content_type("application/json")
|> IdempotencyPlug.call([])
refute conn.halted
refute conn.resp_body
end
test "does not replay legacy unscoped cache entries" do
key = Ecto.UUID.generate()
record = {"legacy-request", "application/json", 200, "secret"}
assert {:ok, true} = Cachex.put(:idempotency_cache, key, record)
conn =
:post
|> conn("/cofe")
|> put_req_header("idempotency-key", key)
|> Conn.put_resp_header("x-request-id", "new-request")
|> Conn.put_resp_content_type("application/json")
|> IdempotencyPlug.call([])
refute conn.halted
refute conn.resp_body
end
test "scopes cached responses to the route and authenticated actor" do
key = Ecto.UUID.generate()
:post
|> conn("/cofe")
|> Conn.assign(:user, %{id: "user-one", is_admin: false, is_moderator: false})
|> put_req_header("idempotency-key", key)
|> Conn.put_resp_header("x-request-id", "original-request")
|> Conn.put_resp_content_type("application/json")
|> IdempotencyPlug.call([])
|> Conn.send_resp(200, "one")
for {path, user} <- [
{"/tea", %{id: "user-one", is_admin: false, is_moderator: false}},
{"/cofe", %{id: "user-two", is_admin: false, is_moderator: false}},
{"/cofe", %{id: "user-one", is_admin: true, is_moderator: false}}
] do
conn =
:post
|> conn(path)
|> Conn.assign(:user, user)
|> put_req_header("idempotency-key", key)
|> Conn.put_resp_header("x-request-id", Ecto.UUID.generate())
|> Conn.put_resp_content_type("application/json")
|> IdempotencyPlug.call([])
refute conn.halted
refute conn.resp_body
end
end
test "scopes cached responses to the OAuth application and token" do
key = Ecto.UUID.generate()
:post
|> conn("/cofe")
|> Conn.assign(:app, %{id: "app-one"})
|> Conn.assign(:token, %{id: "token-one", scopes: ["write"]})
|> put_req_header("idempotency-key", key)
|> Conn.put_resp_header("x-request-id", "original-request")
|> Conn.put_resp_content_type("application/json")
|> IdempotencyPlug.call([])
|> Conn.send_resp(200, "one")
for {app, token} <- [
{%{id: "app-two"}, %{id: "token-two", scopes: ["write"]}},
{%{id: "app-one"}, %{id: "token-two", scopes: ["write"]}},
{%{id: "app-one"}, %{id: "token-one", scopes: ["read"]}}
] do
conn =
:post
|> conn("/cofe")
|> Conn.assign(:app, app)
|> Conn.assign(:token, token)
|> put_req_header("idempotency-key", key)
|> Conn.put_resp_header("x-request-id", Ecto.UUID.generate())
|> Conn.put_resp_content_type("application/json")
|> IdempotencyPlug.call([])
refute conn.halted
refute conn.resp_body
end
end
test "scopes cached responses to configured staff privileges" do
key = Ecto.UUID.generate()
clear_config([:instance, :admin_privileges], [:users_delete])
:post
|> conn("/cofe")
|> Conn.assign(:user, %{id: "admin", is_admin: true, is_moderator: false})
|> put_req_header("idempotency-key", key)
|> Conn.put_resp_header("x-request-id", "original-request")
|> Conn.put_resp_content_type("application/json")
|> IdempotencyPlug.call([])
|> Conn.send_resp(200, "one")
clear_config([:instance, :admin_privileges], [])
conn =
:post
|> conn("/cofe")
|> Conn.assign(:user, %{id: "admin", is_admin: true, is_moderator: false})
|> put_req_header("idempotency-key", key)
|> Conn.put_resp_header("x-request-id", "second-request")
|> Conn.put_resp_content_type("application/json")
|> IdempotencyPlug.call([])
refute conn.halted
refute conn.resp_body
end
test "passes conn downstream if idempotency is not present in headers" do
orig_request_id = "test4"
body = "testing"

View file

@ -0,0 +1,125 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Webhook.NotifyTest do
use Pleroma.DataCase, async: true
alias Pleroma.Webhook
alias Pleroma.Webhook.Notify
import Pleroma.Factory
test "triggers a webhook for a report" do
%{id: activity_id} = activity = insert(:report_activity)
url = "https://example.com/webhook"
Webhook.create(%{url: url, events: [:"report.created"]})
Tesla.Mock.mock(fn %{url: ^url, body: body} = _ ->
report =
body
|> Jason.decode!()
|> Map.get("object")
assert %{"id" => ^activity_id} = report
%Tesla.Env{status: 200, body: ""}
end)
[job] = Notify.trigger_webhooks(activity, :"report.created")
Pleroma.Workers.WebhookWorker.perform(job)
end
test "triggers a webhook for an account" do
%{id: account_id} = user = insert(:user)
url = "https://example.com/webhook"
Webhook.create(%{url: url, events: [:"account.created"]})
Tesla.Mock.mock(fn %{url: ^url, body: body} = _ ->
report =
body
|> Jason.decode!()
|> Map.get("object")
assert %{"id" => ^account_id} = report
%Tesla.Env{status: 200, body: ""}
end)
[job] = Notify.trigger_webhooks(user, :"account.created")
Pleroma.Workers.WebhookWorker.perform(job)
end
test "notifies have a valid signature" do
activity = insert(:report_activity)
{:ok, %{secret: secret} = webhook} =
Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]})
Tesla.Mock.mock(fn %{url: "https://example.com/webhook", body: body, headers: headers} = _ ->
{"X-Hub-Signature", "sha256=" <> signature} =
Enum.find(headers, fn {key, _} -> key == "X-Hub-Signature" end)
assert signature == :crypto.mac(:hmac, :sha256, secret, body) |> Base.encode16(case: :lower)
%Tesla.Env{status: 200, body: ""}
end)
Notify.report_created(webhook, activity)
end
test "returns an error for unsuccessful responses" do
user = insert(:user)
{:ok, webhook} =
Webhook.create(%{url: "https://example.com/webhook", events: [:"account.created"]})
Tesla.Mock.mock(fn %{url: "https://example.com/webhook"} ->
%Tesla.Env{status: 503, body: ""}
end)
assert {:error, {:http_status, 503}} = Notify.account_created(webhook, user)
end
test "accepts any successful response" do
user = insert(:user)
{:ok, webhook} =
Webhook.create(%{url: "https://example.com/webhook", events: [:"account.created"]})
Tesla.Mock.mock(fn %{url: "https://example.com/webhook"} ->
%Tesla.Env{status: 204, body: ""}
end)
assert :ok = Notify.account_created(webhook, user)
end
test "cancels queued deliveries when the webhook was deleted" do
user = insert(:user)
{:ok, webhook} =
Webhook.create(%{url: "https://example.com/webhook", events: [:"account.created"]})
[job] = Notify.trigger_webhooks(user, :"account.created")
Webhook.delete(webhook)
assert {:cancel, :not_found} = Pleroma.Workers.WebhookWorker.perform(job)
end
test "cancels queued deliveries when the webhook was disabled" do
user = insert(:user)
{:ok, webhook} =
Webhook.create(%{url: "https://example.com/webhook", events: [:"account.created"]})
[job] = Notify.trigger_webhooks(user, :"account.created")
Webhook.set_enabled(webhook, false)
assert {:cancel, :disabled} = Pleroma.Workers.WebhookWorker.perform(job)
end
end

View file

@ -0,0 +1,94 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.WebhookTest do
use Pleroma.DataCase, async: true
alias Pleroma.Webhook
test "creating a webhook" do
{:ok, %{id: id}} =
Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]})
assert %{url: "https://example.com/webhook"} = Webhook.get(id)
end
test "editing a webhook" do
{:ok, %{id: id} = webhook} =
Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]})
Webhook.update(webhook, %{events: [:"account.created"]})
assert %{events: [:"account.created"]} = Webhook.get(id)
end
test "rejects invalid events" do
assert {:error, _changeset} =
Webhook.create(%{url: "https://example.com/webhook", events: [:"status.created"]})
{:ok, webhook} =
Webhook.create(%{url: "https://example.com/valid-webhook", events: [:"report.created"]})
assert {:error, _changeset} = Webhook.update(webhook, %{events: [:"status.created"]})
end
test "rejects URLs longer than the database column" do
url = "https://example.com/" <> String.duplicate("a", 256)
assert {:error, changeset} =
Webhook.create(%{url: url, events: [:"report.created"]})
assert %{url: ["should be at most 255 character(s)"]} = errors_on(changeset)
end
test "rejects empty events for non-internal webhooks" do
assert {:error, changeset} = Webhook.create(%{url: "https://example.com/webhook", events: []})
assert %{events: ["can't be blank"]} = errors_on(changeset)
{:ok, webhook} =
Webhook.create(%{url: "https://example.com/valid-webhook", events: [:"report.created"]})
assert {:error, changeset} = Webhook.update(webhook, %{events: []})
assert %{events: ["can't be blank"]} = errors_on(changeset)
assert {:ok, _webhook} =
Webhook.create(%{
url: "https://example.com/internal-webhook",
events: [],
internal: true
})
end
test "filter webhooks by type" do
{:ok, %{id: id1}} =
Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]})
{:ok, %{id: id2}} =
Webhook.create(%{
url: "https://example.com/webhook2",
events: [:"account.created", :"report.created"]
})
Webhook.create(%{url: "https://example.com/webhook3", events: [:"account.created"]})
assert [%{id: ^id1}, %{id: ^id2}] = Webhook.get_by_type(:"report.created")
end
test "change webhook state" do
{:ok, %{id: id, enabled: true} = webhook} =
Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]})
Webhook.set_enabled(webhook, false)
assert %{enabled: false} = Webhook.get(id)
end
test "rotate webhook secrets" do
{:ok, %{id: id, secret: secret} = webhook} =
Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]})
Webhook.rotate_secret(webhook)
%{secret: new_secret} = Webhook.get(id)
assert secret != new_secret
end
end

View file

@ -46,3 +46,5 @@ Mox.defmock(Pleroma.SignatureMock, for: Pleroma.Signature.API)
Mox.defmock(Pleroma.Web.ActivityPub.TransmogrifierMock,
for: Pleroma.Web.ActivityPub.Transmogrifier.API
)
Mox.defmock(Pleroma.Search.ParadeDB.ClientMock, for: Pleroma.Search.ParadeDB.Client)

View file

@ -4,7 +4,7 @@
ExUnit.configure(capture_log: true, max_cases: System.schedulers_online())
ExUnit.start(exclude: [:federated])
ExUnit.start(exclude: [:federated, :integration])
if match?({:unix, :darwin}, :os.type()) do
excluded = ExUnit.configuration() |> Keyword.get(:exclude, [])