fix(webhooks): merge develop and harden delivery
This commit is contained in:
commit
03e75d3b3e
254 changed files with 12265 additions and 1143 deletions
5
test/fixtures/config/static_search.config
vendored
Normal file
5
test/fixtures/config/static_search.config
vendored
Normal 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
|
||||
32
test/fixtures/fedidev.fun/attachment_embedded_note.json
vendored
Normal file
32
test/fixtures/fedidev.fun/attachment_embedded_note.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
test/fixtures/fedidev.fun/attachment_payto_link.json
vendored
Normal file
31
test/fixtures/fedidev.fun/attachment_payto_link.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
40
test/fixtures/fedidev.fun/attachment_url_list.json
vendored
Normal file
40
test/fixtures/fedidev.fun/attachment_url_list.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
test/fixtures/fedidev.fun/emoji_icon_string.json
vendored
Normal file
32
test/fixtures/fedidev.fun/emoji_icon_string.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
34
test/fixtures/fedidev.fun/emoji_icon_url_missing_type.json
vendored
Normal file
34
test/fixtures/fedidev.fun/emoji_icon_url_missing_type.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
31
test/fixtures/fedidev.fun/emoji_missing_icon.json
vendored
Normal file
31
test/fixtures/fedidev.fun/emoji_missing_icon.json
vendored
Normal 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:"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
30
test/fixtures/fedidev.fun/in_reply_to_collection.json
vendored
Normal file
30
test/fixtures/fedidev.fun/in_reply_to_collection.json
vendored
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
35
test/fixtures/fedidev.fun/in_reply_to_embedded_note.json
vendored
Normal file
35
test/fixtures/fedidev.fun/in_reply_to_embedded_note.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
28
test/fixtures/fedidev.fun/in_reply_to_link.json
vendored
Normal file
28
test/fixtures/fedidev.fun/in_reply_to_link.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
28
test/fixtures/fedidev.fun/in_reply_to_list.json
vendored
Normal file
28
test/fixtures/fedidev.fun/in_reply_to_list.json
vendored
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
25
test/fixtures/fedidev.fun/missing_attributed_to.json
vendored
Normal file
25
test/fixtures/fedidev.fun/missing_attributed_to.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
|
||||
28
test/fixtures/fedidev.fun/misskey_quote_list.json
vendored
Normal file
28
test/fixtures/fedidev.fun/misskey_quote_list.json
vendored
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
28
test/fixtures/fedidev.fun/object_type_list.json
vendored
Normal file
28
test/fixtures/fedidev.fun/object_type_list.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
28
test/fixtures/fedidev.fun/quote_uri_list.json
vendored
Normal file
28
test/fixtures/fedidev.fun/quote_uri_list.json
vendored
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
28
test/fixtures/fedidev.fun/quote_url_list.json
vendored
Normal file
28
test/fixtures/fedidev.fun/quote_url_list.json
vendored
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
30
test/fixtures/fedidev.fun/tag_hashtag_missing_type.json
vendored
Normal file
30
test/fixtures/fedidev.fun/tag_hashtag_missing_type.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
31
test/fixtures/fedidev.fun/tag_mention_missing_type.json
vendored
Normal file
31
test/fixtures/fedidev.fun/tag_mention_missing_type.json
vendored
Normal 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
27
test/fixtures/fedidev.fun/url_list.json
vendored
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -369,5 +377,39 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
|
|||
MixTask.run(["filter_whitelisted", "--force"])
|
||||
assert config_records() == existing
|
||||
end
|
||||
|
||||
test "filters blacklisted settings" do
|
||||
clear_config(:database_config_whitelist, [{:pleroma}])
|
||||
clear_config(:database_config_blacklist, [{:pleroma, :logger}, {:blacklisted_group}])
|
||||
|
||||
insert_config_record(:pleroma, :logger, backends: :console)
|
||||
insert_config_record(:web_push_encryption, :vapid_details, a: 1)
|
||||
insert_config_record(:blacklisted_group, :anything, a: 1)
|
||||
|
||||
MixTask.run(["filter_blacklisted", "--force"])
|
||||
|
||||
assert [
|
||||
%ConfigDB{group: :pleroma, key: :instance},
|
||||
%ConfigDB{group: :pleroma, key: Pleroma.Captcha},
|
||||
%ConfigDB{group: :pleroma2, key: :key2},
|
||||
%ConfigDB{group: :web_push_encryption, key: :vapid_details}
|
||||
] = config_records()
|
||||
end
|
||||
|
||||
test "filter_blacklisted doesn't crash when blacklist is unset" do
|
||||
clear_config(:database_config_blacklist, nil)
|
||||
|
||||
existing = config_records()
|
||||
MixTask.run(["filter_blacklisted", "--force"])
|
||||
assert config_records() == existing
|
||||
end
|
||||
|
||||
test "filter_blacklisted doesn't crash when blacklist is disabled" do
|
||||
clear_config(:database_config_blacklist, false)
|
||||
|
||||
existing = config_records()
|
||||
MixTask.run(["filter_blacklisted", "--force"])
|
||||
assert config_records() == existing
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
39
test/pleroma/application_test.exs
Normal file
39
test/pleroma/application_test.exs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.ApplicationTest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
|
||||
# TODO HACK: Temporarily disabled.
|
||||
# Currently causes a bunch of warnings about :warn being deprecated in logs.
|
||||
# ex_syslogger needs to either be updated upstream or forked.
|
||||
describe "config :pleroma, :logger, backends: [{:ex_syslogger, :ex_syslogger}]" do
|
||||
setup do
|
||||
clear_config([:logger], backends: [{:ex_syslogger, :ex_syslogger}])
|
||||
end
|
||||
|
||||
@tag :skip
|
||||
test "is handled" do
|
||||
log = capture_log(fn -> Pleroma.Application.configure_logger() end)
|
||||
|
||||
assert log =~
|
||||
"Configuration {:ex_syslogger, :ex_syslogger} is incorrect. Use {ExSyslogger, :ex_syslogger} instead!"
|
||||
|
||||
assert log =~ "Successfully added logger backend: {ExSyslogger, :ex_syslogger}"
|
||||
end
|
||||
end
|
||||
|
||||
describe "config :pleroma, :logger, :backends: [:console]" do
|
||||
setup do
|
||||
clear_config([:logger], backends: [:console])
|
||||
end
|
||||
|
||||
test "emits a warning" do
|
||||
assert capture_log(fn -> Pleroma.Application.configure_logger() end) =~
|
||||
":console is no longer considered a backend and is enabled by default"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Config.DeprecationWarningsTest do
|
||||
use ExUnit.Case
|
||||
use ExUnit.Case, async: false
|
||||
use Pleroma.Tests.Helpers
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
|
|
@ -388,4 +388,62 @@ defmodule Pleroma.Config.DeprecationWarningsTest do
|
|||
end) =~
|
||||
"Your config is using the old namespace for the Shoutbox configuration."
|
||||
end
|
||||
|
||||
describe "check_deprecated_logger_config" do
|
||||
setup do
|
||||
initial_console = Application.get_env(:logger, :console, nil)
|
||||
initial_backends = Application.get_env(:logger, :backends, nil)
|
||||
|
||||
# NOTE: The Logger does not get reconfigured when running in the test env,
|
||||
# the added and on_exit removed settings below should have no effect besides
|
||||
# triggering the deprecation warnings
|
||||
Application.put_env(:logger, :console, level: :all)
|
||||
Application.put_env(:logger, :backends, [:console])
|
||||
|
||||
on_exit(fn ->
|
||||
if initial_console do
|
||||
Application.put_env(:logger, :console, initial_console)
|
||||
else
|
||||
Application.delete_env(:logger, :console)
|
||||
end
|
||||
|
||||
if initial_backends do
|
||||
Application.put_env(:logger, :backends, initial_backends)
|
||||
else
|
||||
Application.delete_env(:logger, :backends)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
test "warns on deprecated syntax" do
|
||||
log =
|
||||
capture_log(fn ->
|
||||
Pleroma.Config.DeprecationWarnings.check_deprecated_logger_config()
|
||||
end)
|
||||
|
||||
assert log =~
|
||||
"""
|
||||
!!!DEPRECATION WARNING!!!
|
||||
Your configuration is using deprecated syntax for configuring backends of Elixir's logger.
|
||||
`config :logger, backends: [...]` is deprecated syntax due to changes in Elixir.
|
||||
Please update your configuration at your earliest convenience to use:
|
||||
`config :pleroma, :logger, backends: [...]`
|
||||
|
||||
Note: `:console` is no longer considered a backend and is used by default, you can disable it using:
|
||||
`config :logger, :default_handler: false`
|
||||
"""
|
||||
|
||||
assert log =~
|
||||
"""
|
||||
!!!DEPRECATION WARNING!!!
|
||||
Your configuration is using deprecated syntax for configuring logging to console.
|
||||
`config :logger, :console` is deprecated syntax due to changes in Elixir.
|
||||
Please update your configuration at your earliest convenience to use
|
||||
`config :logger, default_handler` and `config :logger, :default_formatter`.
|
||||
|
||||
Note: `:default_handler` is used only for the `level` setting. All other configurations go under
|
||||
`:default_formatter`. For more info visit: https://hexdocs.pm/logger/Logger.html#module-backends-and-backwards-compatibility
|
||||
"""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
179
test/pleroma/gun/conn_test.exs
Normal file
179
test/pleroma/gun/conn_test.exs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Gun.ConnTest do
|
||||
use ExUnit.Case
|
||||
|
||||
import Mox
|
||||
|
||||
alias Pleroma.Gun.Conn
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
setup do
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http} end)
|
||||
stub(Pleroma.GunMock, :await_tunnel_up, fn _, _, _ -> {:ok, :http2} end)
|
||||
stub(Pleroma.GunMock, :close, fn _ -> :ok end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "selects the direct transport from the URI scheme" do
|
||||
expect(Pleroma.GunMock, :open, 2, fn
|
||||
~c"example.com", 8443, %{transport: :tls, tls_opts: tls_opts} ->
|
||||
assert tls_opts[:verify] == :verify_peer
|
||||
{:ok, spawn_link(fn -> Process.sleep(:infinity) end)}
|
||||
|
||||
~c"example.com", 443, %{transport: :tcp} ->
|
||||
{:ok, spawn_link(fn -> Process.sleep(:infinity) end)}
|
||||
end)
|
||||
|
||||
assert {:ok, https_conn, :http, nil} = Conn.open(URI.parse("https://example.com:8443"), [])
|
||||
assert {:ok, http_conn, :http, nil} = Conn.open(URI.parse("http://example.com:443"), [])
|
||||
|
||||
stop_conn(https_conn)
|
||||
stop_conn(http_conn)
|
||||
end
|
||||
|
||||
test "passes authentication to an HTTP CONNECT proxy" do
|
||||
conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||
stream = make_ref()
|
||||
|
||||
expect(Pleroma.GunMock, :open, fn ~c"proxy.example", 8080, opts ->
|
||||
assert opts.transport == :tcp
|
||||
refute Map.has_key?(opts, :tls_opts)
|
||||
{:ok, conn}
|
||||
end)
|
||||
|
||||
expect(Pleroma.GunMock, :connect, fn ^conn, connect_opts ->
|
||||
assert connect_opts.username == "alice"
|
||||
assert connect_opts.password == "secret"
|
||||
assert connect_opts.transport == :tls
|
||||
assert connect_opts.protocols == [:http2, :http]
|
||||
stream
|
||||
end)
|
||||
|
||||
expect(Pleroma.GunMock, :await, fn ^conn, ^stream -> {:response, :fin, 200, []} end)
|
||||
|
||||
assert {:ok, ^conn, :http2, ^stream} =
|
||||
Conn.open(URI.parse("https://origin.example/inbox"),
|
||||
proxy: {~c"proxy.example", 8080},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
)
|
||||
|
||||
stop_conn(conn)
|
||||
end
|
||||
|
||||
test "uses separate TLS options for a TLS CONNECT proxy" do
|
||||
conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||
stream = make_ref()
|
||||
|
||||
expect(Pleroma.GunMock, :open, fn ~c"proxy.example", 8443, opts ->
|
||||
assert opts.transport == :tls
|
||||
assert opts.tls_opts == [verify: :verify_none]
|
||||
{:ok, conn}
|
||||
end)
|
||||
|
||||
expect(Pleroma.GunMock, :connect, fn ^conn, _connect_opts -> stream end)
|
||||
expect(Pleroma.GunMock, :await, fn ^conn, ^stream -> {:response, :fin, 200, []} end)
|
||||
|
||||
assert {:ok, ^conn, :http2, ^stream} =
|
||||
Conn.open(URI.parse("https://origin.example/inbox"),
|
||||
proxy: {~c"proxy.example", 8443},
|
||||
transport: :tls,
|
||||
proxy_tls_opts: [verify: :verify_none]
|
||||
)
|
||||
|
||||
stop_conn(conn)
|
||||
end
|
||||
|
||||
test "uses HTTP/1 for a TLS forwarding proxy" do
|
||||
conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||
|
||||
expect(Pleroma.GunMock, :open, fn ~c"proxy.example", 8443, opts ->
|
||||
assert opts.transport == :tls
|
||||
assert opts.tls_opts == [verify: :verify_none]
|
||||
assert opts.protocols == [:http]
|
||||
{:ok, conn}
|
||||
end)
|
||||
|
||||
assert {:ok, ^conn, :http, :forward_proxy} =
|
||||
Conn.open(URI.parse("http://origin.example/inbox"),
|
||||
proxy: {~c"proxy.example", 8443},
|
||||
transport: :tls,
|
||||
proxy_tls_opts: [verify: :verify_none]
|
||||
)
|
||||
|
||||
stop_conn(conn)
|
||||
end
|
||||
|
||||
test "passes authentication and remote DNS destination to a SOCKS5 proxy" do
|
||||
conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||
|
||||
expect(Pleroma.GunMock, :open, fn ~c"proxy.example", 1080, opts ->
|
||||
assert opts.protocols == [:socks]
|
||||
assert opts.transport == :tcp
|
||||
assert opts.socks_opts.host == ~c"origin.example"
|
||||
assert opts.socks_opts.port == 443
|
||||
assert opts.socks_opts.auth == [{:username_password, "alice", "secret"}]
|
||||
assert opts.socks_opts.transport == :tls
|
||||
assert opts.socks_opts.protocols == [:http2, :http]
|
||||
refute Map.has_key?(opts, :tls_opts)
|
||||
{:ok, conn}
|
||||
end)
|
||||
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :socks} end)
|
||||
|
||||
assert {:ok, ^conn, :http2, nil} =
|
||||
Conn.open(URI.parse("https://origin.example/inbox"),
|
||||
proxy: {:socks5, ~c"proxy.example", 1080},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
)
|
||||
|
||||
stop_conn(conn)
|
||||
end
|
||||
|
||||
test "closes a CONNECT socket when proxy authentication fails" do
|
||||
conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||
stream = make_ref()
|
||||
|
||||
expect(Pleroma.GunMock, :open, fn _, _, _ -> {:ok, conn} end)
|
||||
expect(Pleroma.GunMock, :connect, fn ^conn, _ -> stream end)
|
||||
expect(Pleroma.GunMock, :await, fn ^conn, ^stream -> {:response, :nofin, 407, []} end)
|
||||
expect(Pleroma.GunMock, :close, fn ^conn -> Process.exit(conn, :normal) end)
|
||||
|
||||
assert {:error, :proxy_auth_failed} =
|
||||
Conn.open(URI.parse("https://origin.example/inbox"),
|
||||
proxy: {~c"proxy.example", 8080},
|
||||
proxy_auth: {"alice", "wrong"}
|
||||
)
|
||||
end
|
||||
|
||||
test "returns the proxy status when CONNECT is forbidden" do
|
||||
conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||
stream = make_ref()
|
||||
|
||||
expect(Pleroma.GunMock, :open, fn _, _, _ -> {:ok, conn} end)
|
||||
expect(Pleroma.GunMock, :connect, fn ^conn, _ -> stream end)
|
||||
expect(Pleroma.GunMock, :await, fn ^conn, ^stream -> {:response, :fin, 403, []} end)
|
||||
expect(Pleroma.GunMock, :close, fn ^conn -> Process.exit(conn, :normal) end)
|
||||
|
||||
assert {:error, {:proxy_connect_failed, 403}} =
|
||||
Conn.open(URI.parse("https://origin.example/inbox"),
|
||||
proxy: {~c"proxy.example", 8080}
|
||||
)
|
||||
end
|
||||
|
||||
test "rejects SOCKS4 before opening a socket" do
|
||||
assert {:error, :socks4_unsupported} =
|
||||
Conn.open(URI.parse("https://origin.example/inbox"),
|
||||
proxy: {:socks4, ~c"proxy.example", 1080}
|
||||
)
|
||||
end
|
||||
|
||||
defp stop_conn(conn) do
|
||||
Process.unlink(conn)
|
||||
Process.exit(conn, :kill)
|
||||
end
|
||||
end
|
||||
|
|
@ -11,8 +11,11 @@ defmodule Pleroma.Gun.ConnectionPoolTest do
|
|||
|
||||
defp gun_mock(_) do
|
||||
Pleroma.GunMock
|
||||
|> stub(:open, fn _, _, _ -> Task.start_link(fn -> Process.sleep(100) end) end)
|
||||
|> stub(:open, fn _, _, _ -> Task.start_link(fn -> Process.sleep(:infinity) end) end)
|
||||
|> stub(:await_up, fn _, _ -> {:ok, :http} end)
|
||||
|> stub(:connect, fn _, _ -> make_ref() end)
|
||||
|> stub(:await, fn _, _ -> {:response, :fin, 200, []} end)
|
||||
|> stub(:await_tunnel_up, fn _, _, _ -> {:ok, :http2} end)
|
||||
|> stub(:set_owner, fn _, _ -> :ok end)
|
||||
|
||||
:ok
|
||||
|
|
@ -20,40 +23,213 @@ defmodule Pleroma.Gun.ConnectionPoolTest do
|
|||
|
||||
setup :gun_mock
|
||||
|
||||
test "gives the same connection to 2 concurrent requests" do
|
||||
Enum.map(
|
||||
[
|
||||
"http://www.korean-books.com.kp/KBMbooks/en/periodic/pictorial/20200530163914.pdf",
|
||||
"http://www.korean-books.com.kp/KBMbooks/en/periodic/pictorial/20200528183427.pdf"
|
||||
],
|
||||
fn uri ->
|
||||
uri = URI.parse(uri)
|
||||
task_parent = self()
|
||||
test "opens sibling HTTP/1 connections while an existing connection is in use" do
|
||||
uri = URI.parse("http://www.korean-books.com.kp/document.pdf")
|
||||
|
||||
Task.start_link(fn ->
|
||||
{:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
ConnectionPool.release_conn(conn)
|
||||
send(task_parent, conn)
|
||||
end)
|
||||
end
|
||||
)
|
||||
assert {:ok, first} = ConnectionPool.get_conn(uri, [])
|
||||
assert {:ok, second} = ConnectionPool.get_conn(uri, [])
|
||||
assert first != second
|
||||
|
||||
[pid, pid] =
|
||||
for _ <- 1..2 do
|
||||
receive do
|
||||
pid -> pid
|
||||
end
|
||||
assert :ok = ConnectionPool.release_conn(first)
|
||||
assert :ok = ConnectionPool.release_conn(second)
|
||||
end
|
||||
|
||||
test "multiplexes concurrent HTTP/2 leases on one connection" do
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http2} end)
|
||||
uri = URI.parse("https://h2.example/inbox")
|
||||
|
||||
assert {:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
assert {:ok, ^conn} = ConnectionPool.get_conn(uri, [])
|
||||
|
||||
assert :ok = ConnectionPool.release_conn(conn)
|
||||
assert :ok = ConnectionPool.release_conn(conn)
|
||||
assert Process.alive?(conn)
|
||||
end
|
||||
|
||||
test "does not share connections across direct and authenticated proxy routes" do
|
||||
uri = URI.parse("https://routes.example/inbox")
|
||||
|
||||
assert {:ok, direct} = ConnectionPool.get_conn(uri, [])
|
||||
|
||||
assert {:ok, proxied} =
|
||||
ConnectionPool.get_conn(uri,
|
||||
proxy: {~c"proxy.example", 8080},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
)
|
||||
|
||||
assert {:ok, other_credentials} =
|
||||
ConnectionPool.get_conn(uri,
|
||||
proxy: {~c"proxy.example", 8080},
|
||||
proxy_auth: {"bob", "different-secret"}
|
||||
)
|
||||
|
||||
assert Enum.uniq([direct, proxied, other_credentials]) ==
|
||||
[direct, proxied, other_credentials]
|
||||
|
||||
registry_dump = inspect(Registry.select(ConnectionPool, [{{:"$1", :_, :_}, [], [:"$1"]}]))
|
||||
refute registry_dump =~ "secret"
|
||||
|
||||
Enum.each([direct, proxied, other_credentials], &ConnectionPool.release_conn/1)
|
||||
end
|
||||
|
||||
test "coalesces concurrent cold HTTP/2 checkouts behind one connection" do
|
||||
test_pid = self()
|
||||
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ ->
|
||||
send(test_pid, {:awaiting_connection, self()})
|
||||
|
||||
receive do
|
||||
:continue -> {:ok, :http2}
|
||||
end
|
||||
end)
|
||||
|
||||
uri = URI.parse("https://cold-h2.example/inbox")
|
||||
|
||||
tasks =
|
||||
for _ <- 1..10 do
|
||||
Task.async(fn -> ConnectionPool.get_conn(uri, []) end)
|
||||
end
|
||||
|
||||
assert_receive {:awaiting_connection, worker_pid}
|
||||
refute_receive {:awaiting_connection, _other_worker}, 50
|
||||
send(worker_pid, :continue)
|
||||
|
||||
assert [{:ok, conn}] = tasks |> Enum.map(&Task.await/1) |> Enum.uniq()
|
||||
assert Process.alive?(conn)
|
||||
end
|
||||
|
||||
test "shares a cold connection failure with concurrent waiters" do
|
||||
test_pid = self()
|
||||
|
||||
stub(Pleroma.GunMock, :open, fn _, _, _ ->
|
||||
send(test_pid, {:opening_connection, self()})
|
||||
|
||||
receive do
|
||||
:fail -> {:error, :econnrefused}
|
||||
end
|
||||
end)
|
||||
|
||||
uri = URI.parse("https://unreachable.example/inbox")
|
||||
|
||||
tasks =
|
||||
for _ <- 1..10 do
|
||||
Task.async(fn -> ConnectionPool.get_conn(uri, []) end)
|
||||
end
|
||||
|
||||
assert_receive {:opening_connection, worker_pid}
|
||||
refute_receive {:opening_connection, _other_worker}, 50
|
||||
send(worker_pid, :fail)
|
||||
|
||||
assert Enum.all?(tasks, fn task -> match?({:error, _reason}, Task.await(task)) end)
|
||||
refute_receive {:opening_connection, _other_worker}, 50
|
||||
end
|
||||
|
||||
test "bounds many-host workloads and closes released idle workers" do
|
||||
clear_config([:connections_pool, :max_connections]) do
|
||||
clear_config([:connections_pool, :max_connections], 25)
|
||||
clear_config([:connections_pool, :max_idle_time], 5)
|
||||
restart_worker_supervisor()
|
||||
|
||||
on_exit(&restart_worker_supervisor/0)
|
||||
end
|
||||
|
||||
connections =
|
||||
Enum.map(1..25, fn host_number ->
|
||||
uri = URI.parse("https://host-#{host_number}.example/inbox")
|
||||
assert {:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
conn
|
||||
end)
|
||||
|
||||
assert DynamicSupervisor.count_children(Pleroma.Gun.ConnectionPool.WorkerSupervisor).active ==
|
||||
25
|
||||
|
||||
assert {:error, :pool_full} =
|
||||
ConnectionPool.get_conn(URI.parse("https://overflow.example/inbox"), [])
|
||||
|
||||
Enum.each(connections, &ConnectionPool.release_conn/1)
|
||||
|
||||
assert eventually(fn ->
|
||||
DynamicSupervisor.count_children(Pleroma.Gun.ConnectionPool.WorkerSupervisor).active ==
|
||||
0
|
||||
end)
|
||||
end
|
||||
|
||||
test "does not reclaim a worker that became active after idle selection" do
|
||||
uri = URI.parse("https://reclaim-race.example/inbox")
|
||||
|
||||
assert {:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
assert :ok = ConnectionPool.release_conn(conn)
|
||||
worker = worker_for_conn(conn)
|
||||
|
||||
assert {:idle, _crf, _last_reference} = GenServer.call(worker, :reclaim_info)
|
||||
assert {:ok, ^conn} = ConnectionPool.get_conn(uri, [])
|
||||
assert :in_use = GenServer.call(worker, :reclaim)
|
||||
assert Process.alive?(worker)
|
||||
|
||||
assert :ok = ConnectionPool.release_conn(conn)
|
||||
end
|
||||
|
||||
test "cancels one HTTP/2 stream without discarding the connection" do
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http2} end)
|
||||
uri = URI.parse("https://cancel-h2.example/inbox")
|
||||
stream = make_ref()
|
||||
|
||||
expect(Pleroma.GunMock, :cancel, fn conn, ^stream when is_pid(conn) -> :ok end)
|
||||
|
||||
assert {:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
assert :ok = ConnectionPool.register_stream(conn, stream)
|
||||
assert :ok = ConnectionPool.cancel_stream(conn, stream)
|
||||
assert {:ok, ^conn} = ConnectionPool.get_conn(uri, [])
|
||||
assert :ok = ConnectionPool.release_conn(conn)
|
||||
end
|
||||
|
||||
test "ignores a stale Gun error after cancelling an HTTP/2 stream" do
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http2} end)
|
||||
uri = URI.parse("https://stale-cancel-h2.example/inbox")
|
||||
stream = make_ref()
|
||||
|
||||
expect(Pleroma.GunMock, :cancel, fn conn, ^stream when is_pid(conn) -> :ok end)
|
||||
|
||||
assert {:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
worker = worker_for_conn(conn)
|
||||
assert :ok = ConnectionPool.register_stream(conn, stream)
|
||||
assert :ok = ConnectionPool.cancel_stream(conn, stream)
|
||||
|
||||
send(worker, {:gun_error, conn, stream, {:badstate, "The stream cannot be found."}})
|
||||
assert %{clients: %{}} = :sys.get_state(worker)
|
||||
assert Process.alive?(worker)
|
||||
|
||||
assert {:ok, ^conn} = ConnectionPool.get_conn(uri, [])
|
||||
assert :ok = ConnectionPool.release_conn(conn)
|
||||
end
|
||||
|
||||
test "stops an HTTP/1 worker when a client dies with an active stream" do
|
||||
uri = URI.parse("http://dead-stream.example/media")
|
||||
parent = self()
|
||||
|
||||
client =
|
||||
spawn(fn ->
|
||||
{:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
:ok = ConnectionPool.register_stream(conn, make_ref())
|
||||
send(parent, {:active_stream, conn})
|
||||
Process.sleep(:infinity)
|
||||
end)
|
||||
|
||||
assert_receive {:active_stream, conn}
|
||||
worker = worker_for_conn(conn)
|
||||
Process.exit(client, :kill)
|
||||
|
||||
assert eventually(fn -> not Process.alive?(worker) end)
|
||||
end
|
||||
|
||||
test "connection limit is respected with concurrent requests" do
|
||||
clear_config([:connections_pool, :max_connections]) do
|
||||
clear_config([:connections_pool, :max_connections], 1)
|
||||
# The supervisor needs a reboot to apply the new config setting
|
||||
Process.exit(Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor), :kill)
|
||||
restart_worker_supervisor()
|
||||
|
||||
on_exit(fn ->
|
||||
Process.exit(Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor), :kill)
|
||||
restart_worker_supervisor()
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
@ -96,4 +272,41 @@ defmodule Pleroma.Gun.ConnectionPoolTest do
|
|||
|> Enum.sort()
|
||||
end)
|
||||
end
|
||||
|
||||
defp restart_worker_supervisor do
|
||||
supervisor = Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor)
|
||||
monitor = Process.monitor(supervisor)
|
||||
Process.exit(supervisor, :kill)
|
||||
|
||||
assert_receive {:DOWN, ^monitor, :process, ^supervisor, :killed}
|
||||
|
||||
assert eventually(fn ->
|
||||
case Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor) do
|
||||
pid when is_pid(pid) -> pid != supervisor and Process.alive?(pid)
|
||||
nil -> false
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp worker_for_conn(conn) do
|
||||
[worker] =
|
||||
Registry.select(ConnectionPool, [
|
||||
{{:_, :"$1", {:"$2", :_}}, [{:==, :"$2", conn}], [:"$1"]}
|
||||
])
|
||||
|
||||
worker
|
||||
end
|
||||
|
||||
defp eventually(predicate, attempts \\ 50)
|
||||
|
||||
defp eventually(_predicate, 0), do: false
|
||||
|
||||
defp eventually(predicate, attempts) do
|
||||
if predicate.() do
|
||||
true
|
||||
else
|
||||
Process.sleep(10)
|
||||
eventually(predicate, attempts - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -73,5 +73,11 @@ defmodule Pleroma.HTTP.AdapterHelper.GunTest do
|
|||
|
||||
assert opts[:proxy] == {~c"example.com", 4321}
|
||||
end
|
||||
|
||||
test "uses managed chunks for streamed responses" do
|
||||
opts = Gun.options([stream: true], URI.parse("https://example.com"))
|
||||
|
||||
assert opts[:body_as] == :chunks
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
808
test/pleroma/http/gun_integration_test.exs
Normal file
808
test/pleroma/http/gun_integration_test.exs
Normal file
|
|
@ -0,0 +1,808 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.GunIntegrationTest do
|
||||
use ExUnit.Case, async: false
|
||||
use Pleroma.Tests.Helpers
|
||||
|
||||
alias Pleroma.Tesla.Middleware.ConnectionPool
|
||||
|
||||
setup do
|
||||
clear_config([Pleroma.Gun], Pleroma.Gun.API)
|
||||
clear_config([:connections_pool, :max_idle_time], 1_000)
|
||||
|
||||
origin = start_tls_origin(self())
|
||||
connect_proxy = start_connect_proxy(self())
|
||||
forward_proxy = start_forward_proxy(self())
|
||||
socks_proxy = start_socks5_proxy(self())
|
||||
|
||||
on_exit(fn ->
|
||||
Process.sleep(20)
|
||||
stop_server(socks_proxy)
|
||||
stop_server(forward_proxy)
|
||||
stop_server(connect_proxy)
|
||||
stop_server(origin)
|
||||
end)
|
||||
|
||||
{:ok,
|
||||
origin: origin,
|
||||
connect_proxy: connect_proxy,
|
||||
forward_proxy: forward_proxy,
|
||||
socks_proxy: socks_proxy}
|
||||
end
|
||||
|
||||
test "requests HTTPS directly on a non-standard port", %{origin: origin} do
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(origin.base_url <> "/final")
|
||||
end
|
||||
|
||||
test "follows redirects through an authenticated CONNECT proxy", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
|
||||
request(origin.base_url <> "/redirect", proxy_opts, true)
|
||||
|
||||
expected = "Basic " <> Base.encode64("alice:secret")
|
||||
assert_receive {:connect_proxy, ^expected, "127.0.0.1", origin_port}
|
||||
assert origin_port == origin.port
|
||||
end
|
||||
|
||||
test "requests through an authenticated CONNECT proxy", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
result = request(origin.base_url <> "/final", proxy_opts)
|
||||
|
||||
expected = "Basic " <> Base.encode64("alice:secret")
|
||||
assert_receive {:connect_proxy, ^expected, "127.0.0.1", origin_port}
|
||||
assert origin_port == origin.port
|
||||
assert worker_protocol(origin.port) == :http
|
||||
assert_receive {:origin_request, "/final"}
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = result
|
||||
end
|
||||
|
||||
test "requests HTTP through a forwarding proxy", %{forward_proxy: proxy} do
|
||||
proxy_opts = [proxy: {~c"127.0.0.1", proxy.port}]
|
||||
url = "http://origin.example:8080/final?key=value"
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(url, proxy_opts)
|
||||
|
||||
assert_receive {:forward_proxy, ^url, "origin.example:8080", nil}
|
||||
end
|
||||
|
||||
test "authenticates HTTP requests to a forwarding proxy", %{forward_proxy: proxy} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
url = "http://origin.example/final"
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(url, proxy_opts)
|
||||
|
||||
expected = "Basic " <> Base.encode64("alice:secret")
|
||||
assert_receive {:forward_proxy, ^url, "origin.example", ^expected}
|
||||
end
|
||||
|
||||
test "opens a request-ready authenticated CONNECT tunnel", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
uri = URI.parse(origin.base_url <> "/final")
|
||||
|
||||
opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"},
|
||||
tls_opts: [verify: :verify_none],
|
||||
connect_timeout: 2_000
|
||||
]
|
||||
|
||||
assert {:ok, conn, :http, tunnel} = Pleroma.Gun.Conn.open(uri, opts)
|
||||
stream = :gun.get(conn, "/final", [], %{tunnel: tunnel})
|
||||
assert {:response, :nofin, 200, _headers} = :gun.await(conn, stream, 2_000)
|
||||
assert {:ok, "ok"} = :gun.await_body(conn, stream, 2_000)
|
||||
:gun.close(conn)
|
||||
end
|
||||
|
||||
test "streams chunks through an authenticated CONNECT tunnel", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: client}} =
|
||||
request_chunks(origin.base_url <> "/chunks", proxy_opts)
|
||||
|
||||
assert collect_chunks(client) == "hello"
|
||||
assert :ok = Pleroma.Gun.ConnectionPool.release_conn(client.pid)
|
||||
end
|
||||
|
||||
test "discards an unfinished HTTP/1 redirect stream before following it", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: client}} =
|
||||
request_chunks(origin.base_url <> "/redirect_chunks", proxy_opts, true)
|
||||
|
||||
assert collect_chunks(client) == "hello"
|
||||
assert :ok = Pleroma.Gun.ConnectionPool.release_conn(client.pid)
|
||||
|
||||
expected = "Basic " <> Base.encode64("alice:secret")
|
||||
assert_receive {:connect_proxy, ^expected, "127.0.0.1", _port}
|
||||
assert_receive {:connect_proxy, ^expected, "127.0.0.1", _port}
|
||||
end
|
||||
|
||||
test "uploads multipart bodies through an authenticated CONNECT tunnel", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
multipart = Tesla.Multipart.new() |> Tesla.Multipart.add_field("name", "value")
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200}} =
|
||||
request_multipart(origin.base_url <> "/upload", multipart, proxy_opts)
|
||||
|
||||
assert_receive {:origin_headers, "/upload", headers}
|
||||
assert String.downcase(headers) =~ "content-type: multipart/form-data;"
|
||||
assert_receive {:origin_body, "/upload", body}
|
||||
assert body =~ "name=\"name\""
|
||||
assert body =~ "value"
|
||||
end
|
||||
|
||||
test "uploads streamed bodies through an authenticated CONNECT tunnel", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
body = Stream.map(["streamed", "-body"], & &1)
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200}} =
|
||||
request_stream(origin.base_url <> "/upload", body, proxy_opts)
|
||||
|
||||
assert_receive {:origin_body, "/upload", "streamed-body"}
|
||||
end
|
||||
|
||||
test "discards a partial HTTP/1 upload when body enumeration raises", %{origin: origin} do
|
||||
body =
|
||||
Stream.map(["partial", "raise"], fn
|
||||
"raise" -> raise "upload failed"
|
||||
part -> part
|
||||
end)
|
||||
|
||||
assert_raise RuntimeError, "upload failed", fn ->
|
||||
request_stream(origin.base_url <> "/upload", body, [])
|
||||
end
|
||||
|
||||
assert_receive {:origin_headers, "/upload", _headers}
|
||||
assert worker_protocol(origin.port) == nil
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(origin.base_url <> "/final")
|
||||
end
|
||||
|
||||
test "releases the lease when Gun rejects an invalid request header", %{origin: origin} do
|
||||
adapter_opts = [
|
||||
tls_opts: [verify: :verify_none],
|
||||
connect_timeout: 2_000,
|
||||
timeout: 2_000
|
||||
]
|
||||
|
||||
client = Tesla.client([ConnectionPool], Tesla.Adapter.Gun)
|
||||
|
||||
assert {:invalid_request_header, "x-test", _message} =
|
||||
catch_error(
|
||||
Tesla.get(client, origin.base_url <> "/final",
|
||||
headers: [{"x-test", "value\r\nx-injected: true"}],
|
||||
opts: [adapter: adapter_opts]
|
||||
)
|
||||
)
|
||||
|
||||
assert worker_clients(origin.port) == %{}
|
||||
refute_receive {:origin_request, "/final"}
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(origin.base_url <> "/final")
|
||||
end
|
||||
|
||||
test "requests through authenticated SOCKS5 with proxy-side DNS", %{
|
||||
origin: origin,
|
||||
socks_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {:socks5, ~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
url = "https://localhost:#{origin.port}/final"
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(url, proxy_opts)
|
||||
|
||||
assert_receive {:socks5_proxy, "alice", "secret", "localhost", origin_port}
|
||||
assert origin_port == origin.port
|
||||
end
|
||||
|
||||
test "requests through unauthenticated SOCKS5 with an IP proxy host", %{
|
||||
origin: origin,
|
||||
socks_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [proxy: {:socks5, {127, 0, 0, 1}, proxy.port}]
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
|
||||
request(origin.base_url <> "/final", proxy_opts)
|
||||
|
||||
assert_receive {:socks5_proxy, nil, nil, "127.0.0.1", origin_port}
|
||||
assert origin_port == origin.port
|
||||
end
|
||||
|
||||
test "requests through unauthenticated SOCKS5 with a localhost proxy host", %{
|
||||
origin: origin,
|
||||
socks_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [proxy: {:socks5, ~c"localhost", proxy.port}]
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
|
||||
request(origin.base_url <> "/final", proxy_opts)
|
||||
|
||||
assert_receive {:socks5_proxy, nil, nil, "127.0.0.1", origin_port}
|
||||
assert origin_port == origin.port
|
||||
end
|
||||
|
||||
defp request(url, proxy_opts \\ [], follow_redirects \\ false) do
|
||||
middleware =
|
||||
if follow_redirects do
|
||||
[Tesla.Middleware.FollowRedirects, ConnectionPool]
|
||||
else
|
||||
[ConnectionPool]
|
||||
end
|
||||
|
||||
adapter_opts =
|
||||
Keyword.merge(
|
||||
[tls_opts: [verify: :verify_none], connect_timeout: 2_000, timeout: 2_000],
|
||||
proxy_opts
|
||||
)
|
||||
|
||||
middleware
|
||||
|> Tesla.client(Tesla.Adapter.Gun)
|
||||
|> Tesla.get(url, opts: [adapter: adapter_opts])
|
||||
end
|
||||
|
||||
defp request_chunks(url, proxy_opts, follow_redirects \\ false) do
|
||||
adapter_opts =
|
||||
Keyword.merge(
|
||||
[
|
||||
body_as: :chunks,
|
||||
tls_opts: [verify: :verify_none],
|
||||
connect_timeout: 2_000,
|
||||
timeout: 2_000
|
||||
],
|
||||
proxy_opts
|
||||
)
|
||||
|
||||
middleware =
|
||||
if follow_redirects do
|
||||
[Tesla.Middleware.FollowRedirects, ConnectionPool]
|
||||
else
|
||||
[ConnectionPool]
|
||||
end
|
||||
|
||||
middleware
|
||||
|> Tesla.client(Tesla.Adapter.Gun)
|
||||
|> Tesla.get(url, opts: [adapter: adapter_opts])
|
||||
end
|
||||
|
||||
defp request_multipart(url, multipart, proxy_opts) do
|
||||
adapter_opts =
|
||||
Keyword.merge(
|
||||
[tls_opts: [verify: :verify_none], connect_timeout: 2_000, timeout: 2_000],
|
||||
proxy_opts
|
||||
)
|
||||
|
||||
[ConnectionPool]
|
||||
|> Tesla.client(Tesla.Adapter.Gun)
|
||||
|> Tesla.post(url, multipart, opts: [adapter: adapter_opts])
|
||||
end
|
||||
|
||||
defp request_stream(url, body, proxy_opts) do
|
||||
adapter_opts =
|
||||
Keyword.merge(
|
||||
[tls_opts: [verify: :verify_none], connect_timeout: 2_000, timeout: 2_000],
|
||||
proxy_opts
|
||||
)
|
||||
|
||||
[ConnectionPool]
|
||||
|> Tesla.client(Tesla.Adapter.Gun)
|
||||
|> Tesla.post(url, body, opts: [adapter: adapter_opts])
|
||||
end
|
||||
|
||||
defp collect_chunks(client, acc \\ "") do
|
||||
case Tesla.Adapter.Gun.read_chunk(client.pid, client.stream, client.opts) do
|
||||
{:nofin, chunk} -> collect_chunks(client, acc <> chunk)
|
||||
{:fin, chunk} -> acc <> chunk
|
||||
end
|
||||
end
|
||||
|
||||
defp start_tls_origin(parent) do
|
||||
certfile = Path.expand("../../fixtures/server.pem", __DIR__)
|
||||
keyfile = Path.expand("../../fixtures/private_key.pem", __DIR__)
|
||||
|
||||
{:ok, listener} =
|
||||
:ssl.listen(0, [
|
||||
:binary,
|
||||
certfile: certfile,
|
||||
keyfile: keyfile,
|
||||
reuseaddr: true,
|
||||
active: false,
|
||||
packet: :raw,
|
||||
ip: {127, 0, 0, 1}
|
||||
])
|
||||
|
||||
{:ok, {{127, 0, 0, 1}, port}} = :ssl.sockname(listener)
|
||||
{:ok, acceptor} = Task.start_link(fn -> accept_tls_loop(listener, parent) end)
|
||||
|
||||
%{
|
||||
listener: listener,
|
||||
acceptor: acceptor,
|
||||
transport: :ssl,
|
||||
port: port,
|
||||
base_url: "https://127.0.0.1:#{port}"
|
||||
}
|
||||
end
|
||||
|
||||
defp accept_tls_loop(listener, parent) do
|
||||
case :ssl.transport_accept(listener) do
|
||||
{:ok, tcp_socket} ->
|
||||
case :ssl.handshake(tcp_socket, 2_000) do
|
||||
{:ok, socket} ->
|
||||
pid = spawn(fn -> receive_and_serve_tls(socket, parent) end)
|
||||
:ok = :ssl.controlling_process(socket, pid)
|
||||
send(pid, :serve)
|
||||
|
||||
{:error, _reason} ->
|
||||
:gen_tcp.close(tcp_socket)
|
||||
end
|
||||
|
||||
accept_tls_loop(listener, parent)
|
||||
|
||||
{:error, _reason} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp receive_and_serve_tls(socket, parent) do
|
||||
receive do
|
||||
:serve -> serve_tls(socket, parent)
|
||||
end
|
||||
end
|
||||
|
||||
defp serve_tls(socket, parent) do
|
||||
case recv_headers(:ssl, socket) do
|
||||
{:ok, request} ->
|
||||
{headers, buffered_body} = split_headers(request)
|
||||
path = request_path(headers)
|
||||
send(parent, {:origin_request, path})
|
||||
send(parent, {:origin_headers, path, headers})
|
||||
|
||||
case path do
|
||||
"/redirect" ->
|
||||
send_tls_response(socket, 302, "Found", [{"Location", "/final"}], "")
|
||||
|
||||
"/redirect_chunks" ->
|
||||
send_tls_response(socket, 302, "Found", [{"Location", "/chunks"}], "discard-me")
|
||||
|
||||
"/final" ->
|
||||
send_tls_response(socket, 200, "OK", [], "ok")
|
||||
|
||||
"/chunks" ->
|
||||
send_tls_chunks(socket, ["hel", "lo"])
|
||||
|
||||
"/upload" ->
|
||||
{:ok, body} = recv_request_body(socket, headers, buffered_body)
|
||||
send(parent, {:origin_body, path, body})
|
||||
send_tls_response(socket, 200, "OK", [], "uploaded")
|
||||
|
||||
_ ->
|
||||
send_tls_response(socket, 404, "Not Found", [], "not found")
|
||||
end
|
||||
|
||||
serve_tls(socket, parent)
|
||||
|
||||
{:error, _reason} ->
|
||||
:ssl.close(socket)
|
||||
end
|
||||
end
|
||||
|
||||
defp send_tls_response(socket, status, reason, headers, body) do
|
||||
headers =
|
||||
[
|
||||
{"Content-Length", Integer.to_string(byte_size(body))},
|
||||
{"Connection", "keep-alive"}
|
||||
] ++ headers
|
||||
|
||||
:ssl.send(socket, [
|
||||
"HTTP/1.1 ",
|
||||
Integer.to_string(status),
|
||||
" ",
|
||||
reason,
|
||||
"\r\n",
|
||||
Enum.map(headers, fn {key, value} -> [key, ": ", value, "\r\n"] end),
|
||||
"\r\n",
|
||||
body
|
||||
])
|
||||
end
|
||||
|
||||
defp send_tls_chunks(socket, chunks) do
|
||||
:ssl.send(socket, [
|
||||
"HTTP/1.1 200 OK\r\n",
|
||||
"Transfer-Encoding: chunked\r\n",
|
||||
"Connection: keep-alive\r\n\r\n",
|
||||
Enum.map(chunks, fn chunk ->
|
||||
[Integer.to_string(byte_size(chunk), 16), "\r\n", chunk, "\r\n"]
|
||||
end),
|
||||
"0\r\n\r\n"
|
||||
])
|
||||
end
|
||||
|
||||
defp start_connect_proxy(parent) do
|
||||
start_tcp_server(fn socket -> serve_connect_proxy(socket, parent) end)
|
||||
end
|
||||
|
||||
defp serve_connect_proxy(socket, parent) do
|
||||
with {:ok, headers} <- recv_headers(:gen_tcp, socket),
|
||||
{:ok, host, port} <- parse_connect(headers),
|
||||
auth when is_binary(auth) <- header(headers, "proxy-authorization"),
|
||||
{:ok, upstream} <- tcp_connect(host, port) do
|
||||
send(parent, {:connect_proxy, auth, host, port})
|
||||
:ok = :gen_tcp.send(socket, "HTTP/1.1 200 Connection established\r\n\r\n")
|
||||
tunnel(socket, upstream)
|
||||
else
|
||||
_ ->
|
||||
:gen_tcp.send(
|
||||
socket,
|
||||
"HTTP/1.1 407 Proxy Authentication Required\r\nContent-Length: 0\r\n\r\n"
|
||||
)
|
||||
|
||||
:gen_tcp.close(socket)
|
||||
end
|
||||
end
|
||||
|
||||
defp start_forward_proxy(parent) do
|
||||
start_tcp_server(fn socket -> serve_forward_proxy(socket, parent) end)
|
||||
end
|
||||
|
||||
defp serve_forward_proxy(socket, parent) do
|
||||
with {:ok, request} <- recv_headers(:gen_tcp, socket) do
|
||||
{headers, _buffered_body} = split_headers(request)
|
||||
target = request_path(headers)
|
||||
host = header(headers, "host")
|
||||
auth = header(headers, "proxy-authorization")
|
||||
|
||||
if String.starts_with?(target, "http://") do
|
||||
send(parent, {:forward_proxy, target, host, auth})
|
||||
|
||||
:gen_tcp.send(
|
||||
socket,
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: keep-alive\r\n\r\nok"
|
||||
)
|
||||
|
||||
serve_forward_proxy(socket, parent)
|
||||
else
|
||||
:gen_tcp.send(
|
||||
socket,
|
||||
"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
||||
)
|
||||
|
||||
:gen_tcp.close(socket)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp start_socks5_proxy(parent) do
|
||||
start_tcp_server(fn socket -> serve_socks5_proxy(socket, parent) end)
|
||||
end
|
||||
|
||||
defp serve_socks5_proxy(socket, parent) do
|
||||
with {:ok, <<5, method_count>>} <- :gen_tcp.recv(socket, 2, 2_000),
|
||||
{:ok, methods} <- :gen_tcp.recv(socket, method_count, 2_000),
|
||||
{:ok, username, password} <- negotiate_socks5_auth(socket, methods),
|
||||
{:ok, host, port} <- recv_socks5_destination(socket),
|
||||
{:ok, upstream} <- tcp_connect(host, port),
|
||||
:ok <- :gen_tcp.send(socket, <<5, 0, 0, 1, 0, 0, 0, 0, 0, 0>>) do
|
||||
send(parent, {:socks5_proxy, username, password, host, port})
|
||||
tunnel(socket, upstream)
|
||||
else
|
||||
_ ->
|
||||
:gen_tcp.close(socket)
|
||||
end
|
||||
end
|
||||
|
||||
defp negotiate_socks5_auth(socket, methods) do
|
||||
cond do
|
||||
:binary.match(methods, <<2>>) != :nomatch ->
|
||||
with :ok <- :gen_tcp.send(socket, <<5, 2>>),
|
||||
{:ok, username, password} <- recv_socks5_auth(socket),
|
||||
:ok <- :gen_tcp.send(socket, <<1, 0>>) do
|
||||
{:ok, username, password}
|
||||
end
|
||||
|
||||
:binary.match(methods, <<0>>) != :nomatch ->
|
||||
with :ok <- :gen_tcp.send(socket, <<5, 0>>) do
|
||||
{:ok, nil, nil}
|
||||
end
|
||||
|
||||
true ->
|
||||
{:error, :unsupported_auth}
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_socks5_auth(socket) do
|
||||
with {:ok, <<1, username_length>>} <- :gen_tcp.recv(socket, 2, 2_000),
|
||||
{:ok, username} <- :gen_tcp.recv(socket, username_length, 2_000),
|
||||
{:ok, <<password_length>>} <- :gen_tcp.recv(socket, 1, 2_000),
|
||||
{:ok, password} <- :gen_tcp.recv(socket, password_length, 2_000) do
|
||||
{:ok, username, password}
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_socks5_destination(socket) do
|
||||
with {:ok, <<5, 1, 0, address_type>>} <- :gen_tcp.recv(socket, 4, 2_000),
|
||||
{:ok, host} <- recv_socks5_host(socket, address_type),
|
||||
{:ok, <<port::16>>} <- :gen_tcp.recv(socket, 2, 2_000) do
|
||||
{:ok, host, port}
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_socks5_host(socket, 1) do
|
||||
with {:ok, <<a, b, c, d>>} <- :gen_tcp.recv(socket, 4, 2_000) do
|
||||
{:ok, Enum.join([a, b, c, d], ".")}
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_socks5_host(socket, 3) do
|
||||
with {:ok, <<length>>} <- :gen_tcp.recv(socket, 1, 2_000),
|
||||
{:ok, host} <- :gen_tcp.recv(socket, length, 2_000) do
|
||||
{:ok, host}
|
||||
end
|
||||
end
|
||||
|
||||
defp start_tcp_server(handler) do
|
||||
{:ok, listener} =
|
||||
:gen_tcp.listen(0, [
|
||||
:binary,
|
||||
active: false,
|
||||
packet: :raw,
|
||||
reuseaddr: true,
|
||||
ip: {127, 0, 0, 1}
|
||||
])
|
||||
|
||||
{:ok, {{127, 0, 0, 1}, port}} = :inet.sockname(listener)
|
||||
{:ok, acceptor} = Task.start_link(fn -> accept_tcp_loop(listener, handler) end)
|
||||
%{listener: listener, acceptor: acceptor, transport: :gen_tcp, port: port}
|
||||
end
|
||||
|
||||
defp accept_tcp_loop(listener, handler) do
|
||||
case :gen_tcp.accept(listener) do
|
||||
{:ok, socket} ->
|
||||
pid = spawn(fn -> receive_and_serve_tcp(socket, handler) end)
|
||||
:ok = :gen_tcp.controlling_process(socket, pid)
|
||||
send(pid, :serve)
|
||||
accept_tcp_loop(listener, handler)
|
||||
|
||||
{:error, _reason} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp receive_and_serve_tcp(socket, handler) do
|
||||
receive do
|
||||
:serve -> handler.(socket)
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_headers(transport, socket, acc \\ <<>>) do
|
||||
case transport.recv(socket, 0, 2_000) do
|
||||
{:ok, data} ->
|
||||
acc = acc <> data
|
||||
|
||||
if :binary.match(acc, "\r\n\r\n") == :nomatch do
|
||||
recv_headers(transport, socket, acc)
|
||||
else
|
||||
{:ok, acc}
|
||||
end
|
||||
|
||||
{:error, _reason} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp request_path(headers) do
|
||||
headers
|
||||
|> String.split("\r\n", parts: 2)
|
||||
|> hd()
|
||||
|> String.split(" ")
|
||||
|> Enum.at(1)
|
||||
end
|
||||
|
||||
defp split_headers(request) do
|
||||
{index, 4} = :binary.match(request, "\r\n\r\n")
|
||||
split_at = index + 4
|
||||
<<headers::binary-size(split_at), body::binary>> = request
|
||||
{headers, body}
|
||||
end
|
||||
|
||||
defp recv_request_body(socket, headers, buffered_body) do
|
||||
cond do
|
||||
String.contains?(String.downcase(headers), "transfer-encoding: chunked") ->
|
||||
recv_chunked_body(socket, buffered_body, "")
|
||||
|
||||
content_length = header(headers, "content-length") ->
|
||||
recv_exact(socket, buffered_body, String.to_integer(content_length))
|
||||
|
||||
true ->
|
||||
{:ok, buffered_body}
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_chunked_body(socket, buffer, acc) do
|
||||
with {:ok, line, buffer} <- recv_line(socket, buffer),
|
||||
{size, ""} <- Integer.parse(line, 16) do
|
||||
if size == 0 do
|
||||
{:ok, acc}
|
||||
else
|
||||
with {:ok, chunk_and_crlf, buffer} <- take_bytes(socket, buffer, size + 2),
|
||||
<<chunk::binary-size(size), "\r\n">> <- chunk_and_crlf do
|
||||
recv_chunked_body(socket, buffer, acc <> chunk)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_line(socket, buffer) do
|
||||
case :binary.match(buffer, "\r\n") do
|
||||
{index, 2} ->
|
||||
<<line::binary-size(index), "\r\n", rest::binary>> = buffer
|
||||
{:ok, line, rest}
|
||||
|
||||
:nomatch ->
|
||||
with {:ok, data} <- :ssl.recv(socket, 0, 2_000) do
|
||||
recv_line(socket, buffer <> data)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp take_bytes(_socket, buffer, size) when byte_size(buffer) >= size do
|
||||
<<bytes::binary-size(size), rest::binary>> = buffer
|
||||
{:ok, bytes, rest}
|
||||
end
|
||||
|
||||
defp take_bytes(socket, buffer, size) do
|
||||
with {:ok, data} <- :ssl.recv(socket, 0, 2_000) do
|
||||
take_bytes(socket, buffer <> data, size)
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_exact(socket, buffer, size) do
|
||||
with {:ok, body, _rest} <- take_bytes(socket, buffer, size) do
|
||||
{:ok, body}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_connect(headers) do
|
||||
case headers |> String.split("\r\n", parts: 2) |> hd() |> String.split(" ") do
|
||||
["CONNECT", authority, _protocol] ->
|
||||
case String.split(authority, ":", parts: 2) do
|
||||
[host, port] -> {:ok, host, String.to_integer(port)}
|
||||
_ -> {:error, :invalid_authority}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, :invalid_connect}
|
||||
end
|
||||
end
|
||||
|
||||
defp header(headers, wanted_name) do
|
||||
headers
|
||||
|> String.split("\r\n")
|
||||
|> Enum.find_value(fn line ->
|
||||
case String.split(line, ":", parts: 2) do
|
||||
[name, value] ->
|
||||
if String.downcase(name) == wanted_name, do: String.trim(value)
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp tcp_connect(host, port) do
|
||||
:gen_tcp.connect(
|
||||
String.to_charlist(host),
|
||||
port,
|
||||
[:binary, active: false, packet: :raw],
|
||||
2_000
|
||||
)
|
||||
end
|
||||
|
||||
defp worker_protocol(port) do
|
||||
case worker_state(port) do
|
||||
nil -> nil
|
||||
state -> state.protocol
|
||||
end
|
||||
end
|
||||
|
||||
defp worker_clients(port) do
|
||||
case worker_state(port) do
|
||||
nil -> nil
|
||||
state -> state.clients
|
||||
end
|
||||
end
|
||||
|
||||
defp worker_state(port) do
|
||||
prefix = "https:127.0.0.1:#{port}:"
|
||||
|
||||
Pleroma.Gun.ConnectionPool
|
||||
|> Registry.select([{{:"$1", :"$2", :_}, [], [{{:"$1", :"$2"}}]}])
|
||||
|> Enum.find_value(fn {key, worker} ->
|
||||
if String.starts_with?(key, prefix), do: :sys.get_state(worker)
|
||||
end)
|
||||
end
|
||||
|
||||
defp tunnel(client, upstream) do
|
||||
:ok = :inet.setopts(client, active: :once)
|
||||
:ok = :inet.setopts(upstream, active: :once)
|
||||
tunnel_loop(client, upstream)
|
||||
end
|
||||
|
||||
defp tunnel_loop(client, upstream) do
|
||||
receive do
|
||||
{:tcp, ^client, data} ->
|
||||
:ok = :gen_tcp.send(upstream, data)
|
||||
:ok = :inet.setopts(client, active: :once)
|
||||
tunnel_loop(client, upstream)
|
||||
|
||||
{:tcp, ^upstream, data} ->
|
||||
:ok = :gen_tcp.send(client, data)
|
||||
:ok = :inet.setopts(upstream, active: :once)
|
||||
tunnel_loop(client, upstream)
|
||||
|
||||
{:tcp_closed, _socket} ->
|
||||
:gen_tcp.close(client)
|
||||
:gen_tcp.close(upstream)
|
||||
|
||||
{:tcp_error, _socket, _reason} ->
|
||||
:gen_tcp.close(client)
|
||||
:gen_tcp.close(upstream)
|
||||
after
|
||||
10_000 ->
|
||||
:gen_tcp.close(client)
|
||||
:gen_tcp.close(upstream)
|
||||
end
|
||||
end
|
||||
|
||||
defp stop_server(%{listener: listener, acceptor: acceptor, transport: transport}) do
|
||||
transport.close(listener)
|
||||
|
||||
if Process.alive?(acceptor), do: Process.exit(acceptor, :normal)
|
||||
end
|
||||
end
|
||||
|
|
@ -23,6 +23,29 @@ defmodule Pleroma.ListTest do
|
|||
assert changeset.errors == [title: {"can't be blank", [validation: :required]}]
|
||||
end
|
||||
|
||||
test "creating a list with an emoji" do
|
||||
user = insert(:user)
|
||||
{:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title", emoji: "🕓"}, user)
|
||||
%Pleroma.List{emoji: emoji} = Pleroma.List.get(list.id, user)
|
||||
assert emoji == "🕓"
|
||||
end
|
||||
|
||||
test "validates emoji" do
|
||||
user = insert(:user)
|
||||
|
||||
assert {:error, changeset} =
|
||||
Pleroma.List.create(%{title: "title", emoji: "not an emoji"}, user)
|
||||
|
||||
assert changeset.errors == [emoji: {"Invalid emoji", []}]
|
||||
end
|
||||
|
||||
test "updating a list's emoji" do
|
||||
user = insert(:user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "title"}, user)
|
||||
{:ok, %{emoji: emoji}} = Pleroma.List.update(list, %{emoji: "🕓"})
|
||||
assert "🕓" == emoji
|
||||
end
|
||||
|
||||
test "getting a list not belonging to the user" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
22
test/pleroma/reverse_proxy/client/tesla_test.exs
Normal file
22
test/pleroma/reverse_proxy/client/tesla_test.exs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.ReverseProxy.Client.TeslaTest do
|
||||
use ExUnit.Case
|
||||
|
||||
import Mox
|
||||
|
||||
alias Pleroma.ReverseProxy.Client.Tesla
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
test "cancels an unfinished stream before releasing its connection" do
|
||||
conn = self()
|
||||
stream = [make_ref(), make_ref()]
|
||||
|
||||
expect(Pleroma.GunMock, :cancel, fn ^conn, ^stream -> :ok end)
|
||||
|
||||
assert :ok = Tesla.close(%{pid: conn, stream: stream})
|
||||
end
|
||||
end
|
||||
|
|
@ -115,6 +115,7 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
describe "max_body" do
|
||||
test "length returns error if content-length more than option", %{conn: conn} do
|
||||
request_mock(0)
|
||||
expect(ClientMock, :close, fn _ -> :ok end)
|
||||
|
||||
assert capture_log(fn ->
|
||||
ReverseProxy.call(conn, "/huge-file", max_body_length: 4)
|
||||
|
|
@ -128,14 +129,31 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
end) == ""
|
||||
end
|
||||
|
||||
test "closes streamed responses with invalid status", %{conn: conn} do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/invalid-status", _, _, _ ->
|
||||
{:ok, 404, [], %{url: "/invalid-status"}}
|
||||
end)
|
||||
|> expect(:close, fn %{url: "/invalid-status"} -> :ok end)
|
||||
|
||||
ReverseProxy.call(conn, "/invalid-status")
|
||||
end
|
||||
|
||||
test "max_body_length returns error if streaming body more than that option", %{conn: conn} do
|
||||
stream_mock(3, true)
|
||||
|
||||
assert capture_log(fn ->
|
||||
ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 30)
|
||||
ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 29)
|
||||
end) =~
|
||||
"Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large"
|
||||
end
|
||||
|
||||
test "max_body_length accepts a body exactly at the limit", %{conn: conn} do
|
||||
stream_mock(4)
|
||||
|
||||
conn = ReverseProxy.call(conn, "/stream-bytes/30", max_body_length: 30)
|
||||
assert byte_size(conn.resp_body) == 30
|
||||
end
|
||||
end
|
||||
|
||||
describe "HEAD requests" do
|
||||
|
|
@ -200,7 +218,10 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
|
||||
test "204", %{conn: conn} do
|
||||
url = "/status/204"
|
||||
expect(ClientMock, :request, fn :get, _url, _, _, _ -> {:ok, 204, [], %{}} end)
|
||||
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, _url, _, _, _ -> {:ok, 204, [], %{}} end)
|
||||
|> expect(:close, fn %{} -> :ok end)
|
||||
|
||||
capture_log(fn ->
|
||||
conn = ReverseProxy.call(conn, url)
|
||||
|
|
@ -222,6 +243,56 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"]
|
||||
end
|
||||
|
||||
test "sniffs and preserves an image body with a generic content type", %{conn: conn} do
|
||||
body = File.read!("test/fixtures/image.jpg")
|
||||
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/extensionless", headers, _, _ ->
|
||||
assert {"accept-encoding", "identity"} in headers
|
||||
{:ok, 200, [{"content-type", "application/octet-stream"}], %{body: body}}
|
||||
end)
|
||||
|> expect(:stream_body, fn %{body: ^body} = client ->
|
||||
{:ok, body, Map.delete(client, :body)}
|
||||
end)
|
||||
|> expect(:stream_body, fn %{} -> :done end)
|
||||
|
||||
conn =
|
||||
ReverseProxy.call(conn, "/extensionless",
|
||||
sniff_content_type: true,
|
||||
max_read_duration: :infinity,
|
||||
req_headers: [{"accept-encoding", "gzip"}]
|
||||
)
|
||||
|
||||
assert conn.resp_body == body
|
||||
assert Conn.get_resp_header(conn, "content-type") == ["image/jpeg"]
|
||||
|
||||
assert Conn.get_resp_header(conn, "content-disposition") == [
|
||||
"inline; filename=\"inline.jpg\""
|
||||
]
|
||||
end
|
||||
|
||||
test "preserves a generic content type for a non-image body", %{conn: conn} do
|
||||
body = "not an image"
|
||||
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/extensionless", _, _, _ ->
|
||||
{:ok, 200, [], %{body: body}}
|
||||
end)
|
||||
|> expect(:stream_body, fn %{body: ^body} = client ->
|
||||
{:ok, body, Map.delete(client, :body)}
|
||||
end)
|
||||
|> expect(:stream_body, fn %{} -> :done end)
|
||||
|
||||
conn = ReverseProxy.call(conn, "/extensionless", sniff_content_type: true)
|
||||
|
||||
assert conn.resp_body == body
|
||||
assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"]
|
||||
|
||||
assert Conn.get_resp_header(conn, "content-disposition") == [
|
||||
"attachment; filename=\"extensionless\""
|
||||
]
|
||||
end
|
||||
|
||||
defp headers_mock(_) do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/headers", headers, _, _ ->
|
||||
|
|
@ -328,6 +399,24 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
conn = ReverseProxy.call(conn, "/disposition")
|
||||
|
||||
assert {"content-type", "image/gif"} in conn.resp_headers
|
||||
assert {"content-disposition", "inline; filename=\"inline.gif\""} in conn.resp_headers
|
||||
end
|
||||
|
||||
test "forces inline for inline content types overriding upstream attachment", %{
|
||||
conn: conn
|
||||
} do
|
||||
disposition_headers_mock([
|
||||
{"content-type", "image/png"},
|
||||
{"content-disposition", "attachment; filename=\"filename.png\""},
|
||||
{"content-disposition", "attachment; filename=\"duplicate.png\""},
|
||||
{"content-length", "0"}
|
||||
])
|
||||
|
||||
conn = ReverseProxy.call(conn, "/disposition")
|
||||
|
||||
[disposition] = Conn.get_resp_header(conn, "content-disposition")
|
||||
assert String.starts_with?(disposition, "inline")
|
||||
refute String.starts_with?(disposition, "attachment")
|
||||
end
|
||||
|
||||
test "with content-disposition header", %{conn: conn} do
|
||||
|
|
@ -340,6 +429,50 @@ 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
|
||||
|
|
|
|||
79
test/pleroma/search/backfill_test.exs
Normal file
79
test/pleroma/search/backfill_test.exs
Normal 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
|
||||
323
test/pleroma/search/parade_db_integration_test.exs
Normal file
323
test/pleroma/search/parade_db_integration_test.exs
Normal 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
|
||||
286
test/pleroma/search/parade_db_test.exs
Normal file
286
test/pleroma/search/parade_db_test.exs
Normal 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'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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
99
test/pleroma/tesla/middleware/connection_pool_test.exs
Normal file
99
test/pleroma/tesla/middleware/connection_pool_test.exs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Tesla.Middleware.ConnectionPoolTest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
import Mox
|
||||
|
||||
alias Pleroma.Tesla.Middleware.ConnectionPool
|
||||
|
||||
defmodule FailingAdapter do
|
||||
@behaviour Tesla.Adapter
|
||||
|
||||
@impl Tesla.Adapter
|
||||
def call(_env, _opts), do: {:error, :request_failed}
|
||||
end
|
||||
|
||||
defmodule EmptyAdapter do
|
||||
@behaviour Tesla.Adapter
|
||||
|
||||
@impl Tesla.Adapter
|
||||
def call(env, _opts), do: {:ok, %{env | status: 204, body: ""}}
|
||||
end
|
||||
|
||||
test "discards the connection when the adapter cannot finish a request" do
|
||||
test_pid = self()
|
||||
|
||||
stub(Pleroma.GunMock, :open, fn _, _, _ ->
|
||||
{:ok, conn} = Task.start_link(fn -> Process.sleep(:infinity) end)
|
||||
send(test_pid, {:gun_conn, conn})
|
||||
{:ok, conn}
|
||||
end)
|
||||
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http} end)
|
||||
|
||||
client = Tesla.client([ConnectionPool], FailingAdapter)
|
||||
|
||||
assert {:error, :request_failed} =
|
||||
Tesla.get(client, "http://discard.example/", opts: [adapter: []])
|
||||
|
||||
assert_receive {:gun_conn, conn}
|
||||
|
||||
assert eventually(fn -> not Process.alive?(conn) end)
|
||||
end
|
||||
|
||||
test "releases a chunks lease when the response has no stream" do
|
||||
clear_config([:connections_pool, :max_idle_time], 5)
|
||||
test_pid = self()
|
||||
|
||||
stub(Pleroma.GunMock, :open, fn _, _, _ ->
|
||||
{:ok, conn} = Task.start_link(fn -> Process.sleep(:infinity) end)
|
||||
send(test_pid, {:gun_conn, conn})
|
||||
{:ok, conn}
|
||||
end)
|
||||
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http} end)
|
||||
|
||||
client = Tesla.client([ConnectionPool], EmptyAdapter)
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 204, body: ""}} =
|
||||
Tesla.get(client, "http://empty-chunks.example/",
|
||||
opts: [adapter: [body_as: :chunks]]
|
||||
)
|
||||
|
||||
assert_receive {:gun_conn, conn}
|
||||
worker = worker_for_conn(conn)
|
||||
assert eventually(fn -> not Process.alive?(worker) end)
|
||||
end
|
||||
|
||||
test "rejects unmanaged pooled streams" do
|
||||
client = Tesla.client([ConnectionPool], EmptyAdapter)
|
||||
|
||||
assert {:error, :pooled_streaming_requires_chunks} =
|
||||
Tesla.get(client, "http://stream.example/", opts: [adapter: [body_as: :stream]])
|
||||
end
|
||||
|
||||
defp worker_for_conn(conn) do
|
||||
[worker] =
|
||||
Registry.select(Pleroma.Gun.ConnectionPool, [
|
||||
{{:_, :"$1", {:"$2", :_}}, [{:==, :"$2", conn}], [:"$1"]}
|
||||
])
|
||||
|
||||
worker
|
||||
end
|
||||
|
||||
defp eventually(predicate, attempts \\ 50)
|
||||
|
||||
defp eventually(_predicate, 0), do: false
|
||||
|
||||
defp eventually(predicate, attempts) do
|
||||
if predicate.() do
|
||||
true
|
||||
else
|
||||
Process.sleep(10)
|
||||
eventually(predicate, attempts - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1998,14 +1998,26 @@ defmodule Pleroma.UserTest do
|
|||
end
|
||||
|
||||
describe "caching" do
|
||||
test "set_cache stores friends under the AP ID cache key" do
|
||||
user = insert(:user)
|
||||
|
||||
User.set_cache(user)
|
||||
|
||||
assert {:ok, []} = Cachex.get(:user_cache, "friends_ap_ids:#{user.ap_id}")
|
||||
assert {:ok, nil} = Cachex.get(:user_cache, "friends_ap_ids:#{user.nickname}")
|
||||
end
|
||||
|
||||
test "invalidate_cache works" do
|
||||
user = insert(:user)
|
||||
|
||||
User.set_cache(user)
|
||||
User.get_cached_by_id(user.id)
|
||||
User.invalidate_cache(user)
|
||||
|
||||
{:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
|
||||
{:ok, nil} = Cachex.get(:user_cache, "nickname:#{user.nickname}")
|
||||
{:ok, nil} = Cachex.get(:user_cache, "friends_ap_ids:#{user.ap_id}")
|
||||
{:ok, nil} = Cachex.get(:user_cache, "id:#{user.id}")
|
||||
end
|
||||
|
||||
test "User.delete() plugs any possible zombie objects" do
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunAttachmentsTest do
|
||||
use Oban.Testing, repo: Pleroma.Repo
|
||||
use Pleroma.DataCase
|
||||
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
setup_all do
|
||||
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||
:ok
|
||||
end
|
||||
|
||||
defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do
|
||||
case User.get_cached_by_ap_id(ap_id) do
|
||||
%User{} ->
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
insert(:user,
|
||||
local: false,
|
||||
ap_id: ap_id,
|
||||
follower_address: ap_id <> "/followers",
|
||||
following_address: ap_id <> "/following",
|
||||
featured_address: ap_id <> "/collections/featured",
|
||||
inbox: ap_id <> "/inbox"
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp ingest_data!(data) do
|
||||
ensure_remote_actor!(data["actor"])
|
||||
|
||||
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
|
||||
object = Object.normalize(activity.data["object"], fetch: false)
|
||||
|
||||
{activity, object}
|
||||
end
|
||||
|
||||
defp ingest_fixture!(rel_path) do
|
||||
rel_path
|
||||
|> File.read!()
|
||||
|> Jason.decode!()
|
||||
|> ingest_data!()
|
||||
end
|
||||
|
||||
test "drops non-http(s) attachment href" do
|
||||
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/attachment_payto_link.json")
|
||||
assert object.data["attachment"] == []
|
||||
end
|
||||
|
||||
test "drops embedded non-attachment objects" do
|
||||
{_activity, object} =
|
||||
ingest_fixture!("test/fixtures/fedidev.fun/attachment_embedded_note.json")
|
||||
|
||||
assert object.data["attachment"] == []
|
||||
end
|
||||
|
||||
test "accepts attachment url as list and keeps the first link" do
|
||||
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/attachment_url_list.json")
|
||||
|
||||
[attachment] = object.data["attachment"]
|
||||
[%{"href" => href} | _] = attachment["url"]
|
||||
|
||||
assert href == "https://fedidev.fun/images/007.png"
|
||||
end
|
||||
|
||||
test "drops malformed attachment list entries" do
|
||||
data = File.read!("test/fixtures/fedidev.fun/attachment_url_list.json") |> Jason.decode!()
|
||||
data = update_in(data, ["object", "attachment"], &["malformed" | List.wrap(&1)])
|
||||
|
||||
{_activity, object} = ingest_data!(data)
|
||||
|
||||
assert length(object.data["attachment"]) == 1
|
||||
end
|
||||
|
||||
test "accepts a URI string in an attachment url list" do
|
||||
data = File.read!("test/fixtures/fedidev.fun/attachment_url_list.json") |> Jason.decode!()
|
||||
|
||||
data =
|
||||
put_in(data, ["object", "attachment", "url"], [
|
||||
"https://fedidev.fun/images/007.png"
|
||||
])
|
||||
|
||||
{_activity, object} = ingest_data!(data)
|
||||
|
||||
assert [%{"url" => [%{"href" => "https://fedidev.fun/images/007.png"}]}] =
|
||||
object.data["attachment"]
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunEmojiTest do
|
||||
use Oban.Testing, repo: Pleroma.Repo
|
||||
use Pleroma.DataCase
|
||||
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
setup_all do
|
||||
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||
:ok
|
||||
end
|
||||
|
||||
defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do
|
||||
case User.get_cached_by_ap_id(ap_id) do
|
||||
%User{} ->
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
insert(:user,
|
||||
local: false,
|
||||
ap_id: ap_id,
|
||||
follower_address: ap_id <> "/followers",
|
||||
following_address: ap_id <> "/following",
|
||||
featured_address: ap_id <> "/collections/featured",
|
||||
inbox: ap_id <> "/inbox"
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp ingest_data!(data) do
|
||||
ensure_remote_actor!(data["actor"])
|
||||
|
||||
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
|
||||
object = Object.normalize(activity.data["object"], fetch: false)
|
||||
|
||||
{activity, object}
|
||||
end
|
||||
|
||||
defp ingest_fixture!(rel_path) do
|
||||
rel_path
|
||||
|> File.read!()
|
||||
|> Jason.decode!()
|
||||
|> ingest_data!()
|
||||
end
|
||||
|
||||
test "ingests Emoji tag with string icon" do
|
||||
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/emoji_icon_string.json")
|
||||
assert object.data["emoji"]["cow"] == "https://fedidev.fun/static/cow.png"
|
||||
end
|
||||
|
||||
test "ingests Emoji tag with icon missing type" do
|
||||
{_activity, object} =
|
||||
ingest_fixture!("test/fixtures/fedidev.fun/emoji_icon_url_missing_type.json")
|
||||
|
||||
assert object.data["emoji"]["cow2"] == "https://fedidev.fun/static/cow.png"
|
||||
end
|
||||
|
||||
test "ignores Emoji tag missing icon" do
|
||||
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/emoji_missing_icon.json")
|
||||
assert object.data["emoji"] == %{}
|
||||
end
|
||||
|
||||
test "ignores Emoji tag missing a string name" do
|
||||
data = File.read!("test/fixtures/fedidev.fun/emoji_icon_string.json") |> Jason.decode!()
|
||||
data = put_in(data, ["object", "tag", Access.at(0), "name"], nil)
|
||||
|
||||
{_activity, object} = ingest_data!(data)
|
||||
|
||||
assert object.data["emoji"] == %{}
|
||||
end
|
||||
|
||||
test "ignores Emoji tag with a non-http icon" do
|
||||
data = File.read!("test/fixtures/fedidev.fun/emoji_icon_string.json") |> Jason.decode!()
|
||||
data = put_in(data, ["object", "tag", Access.at(0), "icon"], "file:///tmp/cow.png")
|
||||
|
||||
{_activity, object} = ingest_data!(data)
|
||||
|
||||
assert object.data["emoji"] == %{}
|
||||
end
|
||||
|
||||
test "normalizes a list-typed Emoji tag before building the emoji map" do
|
||||
data = File.read!("test/fixtures/fedidev.fun/emoji_icon_string.json") |> Jason.decode!()
|
||||
|
||||
data =
|
||||
put_in(data, ["object", "tag", Access.at(0), "type"], [
|
||||
"https://example.com/ns#CustomTag",
|
||||
"Emoji"
|
||||
])
|
||||
|
||||
{_activity, object} = ingest_data!(data)
|
||||
|
||||
assert object.data["emoji"]["cow"] == "https://fedidev.fun/static/cow.png"
|
||||
end
|
||||
end
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -220,5 +220,13 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
|
|||
"acceptsChatMessages"
|
||||
)
|
||||
end
|
||||
|
||||
test "it returns false if chat is disabled" do
|
||||
clear_config([Pleroma.Chat, :enabled], false)
|
||||
user = insert(:user, accepts_chat_messages: true)
|
||||
|
||||
assert %{"capabilities" => %{"acceptsChatMessages" => false}} =
|
||||
UserView.render("user.json", user: user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
@ -194,19 +207,9 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
|
||||
setup do: clear_config(:configurable_from_database, true)
|
||||
|
||||
setup do:
|
||||
clear_config(:database_config_whitelist, [
|
||||
{:pleroma},
|
||||
{:http},
|
||||
{:idna},
|
||||
{:oban},
|
||||
{:tesla},
|
||||
{:ueberauth}
|
||||
])
|
||||
|
||||
test "create new config setting in db", %{conn: conn} do
|
||||
ueberauth = Application.get_env(:ueberauth, Ueberauth)
|
||||
on_exit(fn -> Application.put_env(:ueberauth, Ueberauth, ueberauth) end)
|
||||
aws = Application.get_env(:ex_aws, :s3)
|
||||
on_exit(fn -> Application.put_env(:ex_aws, :s3, aws) end)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|
|
@ -215,9 +218,9 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
configs: [
|
||||
%{group: ":pleroma", key: ":key1", value: "value1"},
|
||||
%{
|
||||
group: ":ueberauth",
|
||||
key: "Ueberauth",
|
||||
value: [%{"tuple" => [":consumer_secret", "aaaa"]}]
|
||||
group: ":cors_plug",
|
||||
key: ":key1",
|
||||
value: [%{"tuple" => [":key", "value"]}]
|
||||
},
|
||||
%{
|
||||
group: ":pleroma",
|
||||
|
|
@ -244,9 +247,9 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
value: %{":nested_5" => ":upload", "endpoint" => "https://example.com"}
|
||||
},
|
||||
%{
|
||||
group: ":idna",
|
||||
key: ":key5",
|
||||
value: %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]}
|
||||
group: ":ex_aws",
|
||||
key: ":s3",
|
||||
value: %{"tuple" => ["access_key_id", "aaaa", []]}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
|
@ -260,10 +263,10 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
"db" => [":key1"]
|
||||
},
|
||||
%{
|
||||
"group" => ":ueberauth",
|
||||
"key" => "Ueberauth",
|
||||
"value" => [%{"tuple" => [":consumer_secret", "aaaa"]}],
|
||||
"db" => [":consumer_secret"]
|
||||
"group" => ":cors_plug",
|
||||
"key" => ":key1",
|
||||
"value" => [%{"tuple" => [":key", "value"]}],
|
||||
"db" => [":key"]
|
||||
},
|
||||
%{
|
||||
"group" => ":pleroma",
|
||||
|
|
@ -293,10 +296,10 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
"db" => [":key4"]
|
||||
},
|
||||
%{
|
||||
"group" => ":idna",
|
||||
"key" => ":key5",
|
||||
"value" => %{"tuple" => ["string", "Pleroma.Captcha.NotReal", []]},
|
||||
"db" => [":key5"]
|
||||
"group" => ":ex_aws",
|
||||
"key" => ":s3",
|
||||
"value" => %{"tuple" => ["access_key_id", "aaaa", []]},
|
||||
"db" => [":s3"]
|
||||
}
|
||||
],
|
||||
"need_reboot" => false
|
||||
|
|
@ -322,18 +325,19 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
nested_5: :upload
|
||||
}
|
||||
|
||||
assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []}
|
||||
assert Application.get_env(:cors_plug, :key1) == [key: "value"]
|
||||
assert Application.get_env(:ex_aws, :s3) == {"access_key_id", "aaaa", []}
|
||||
end
|
||||
|
||||
test "save configs setting without explicit key", %{conn: conn} do
|
||||
adapter = Application.get_env(:http, :adapter)
|
||||
send_user_agent = Application.get_env(:http, :send_user_agent)
|
||||
user_agent = Application.get_env(:http, :user_agent)
|
||||
mime = Application.get_env(:mime, :library)
|
||||
prometheus = Application.get_env(:prometheus, Pleroma.Web.Endpoint.MetricsExporter)
|
||||
web_push = Application.get_env(:web_push_encryption, :vapid_details)
|
||||
|
||||
on_exit(fn ->
|
||||
Application.put_env(:http, :adapter, adapter)
|
||||
Application.put_env(:http, :send_user_agent, send_user_agent)
|
||||
Application.put_env(:http, :user_agent, user_agent)
|
||||
Application.put_env(:mime, :library, mime)
|
||||
Application.put_env(:prometheus, Pleroma.Web.Endpoint.MetricsExporter, prometheus)
|
||||
Application.put_env(:web_push_encryption, :vapid_details, web_push)
|
||||
end)
|
||||
|
||||
conn =
|
||||
|
|
@ -342,19 +346,19 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
|> post("/api/pleroma/admin/config", %{
|
||||
configs: [
|
||||
%{
|
||||
group: ":http",
|
||||
key: ":adapter",
|
||||
group: ":mime",
|
||||
key: ":library",
|
||||
value: [":someval"]
|
||||
},
|
||||
%{
|
||||
group: ":http",
|
||||
key: ":send_user_agent",
|
||||
group: ":prometheus",
|
||||
key: "Pleroma.Web.Endpoint.MetricsExporter",
|
||||
value: true
|
||||
},
|
||||
%{
|
||||
group: ":http",
|
||||
key: ":user_agent",
|
||||
value: [":default"]
|
||||
group: ":web_push_encryption",
|
||||
key: ":vapid_details",
|
||||
value: [":nope"]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
|
@ -362,30 +366,30 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
assert json_response_and_validate_schema(conn, 200) == %{
|
||||
"configs" => [
|
||||
%{
|
||||
"group" => ":http",
|
||||
"key" => ":adapter",
|
||||
"group" => ":mime",
|
||||
"key" => ":library",
|
||||
"value" => [":someval"],
|
||||
"db" => [":adapter"]
|
||||
"db" => [":library"]
|
||||
},
|
||||
%{
|
||||
"group" => ":http",
|
||||
"key" => ":send_user_agent",
|
||||
"group" => ":prometheus",
|
||||
"key" => "Pleroma.Web.Endpoint.MetricsExporter",
|
||||
"value" => true,
|
||||
"db" => [":send_user_agent"]
|
||||
"db" => ["Pleroma.Web.Endpoint.MetricsExporter"]
|
||||
},
|
||||
%{
|
||||
"group" => ":http",
|
||||
"key" => ":user_agent",
|
||||
"value" => [":default"],
|
||||
"db" => [":user_agent"]
|
||||
"group" => ":web_push_encryption",
|
||||
"key" => ":vapid_details",
|
||||
"value" => [":nope"],
|
||||
"db" => [":vapid_details"]
|
||||
}
|
||||
],
|
||||
"need_reboot" => false
|
||||
}
|
||||
|
||||
assert Application.get_env(:http, :adapter) == [:someval]
|
||||
assert Application.get_env(:http, :send_user_agent) == true
|
||||
assert Application.get_env(:http, :user_agent) == [:default]
|
||||
assert Application.get_env(:mime, :library) == [:someval]
|
||||
assert Application.get_env(:prometheus, Pleroma.Web.Endpoint.MetricsExporter) == true
|
||||
assert Application.get_env(:web_push_encryption, :vapid_details) == [:nope]
|
||||
end
|
||||
|
||||
test "saving config with partial update", %{conn: conn} do
|
||||
|
|
@ -631,17 +635,17 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/pleroma/admin/config", %{
|
||||
configs: [
|
||||
%{group: ":tesla", key: ":adapter", value: "Tesla.Adapter.Httpc"}
|
||||
%{group: ":pleroma", key: ":key1", value: "Pleroma.Web.Endpoint"}
|
||||
]
|
||||
})
|
||||
|
||||
assert json_response_and_validate_schema(conn, 200) == %{
|
||||
"configs" => [
|
||||
%{
|
||||
"group" => ":tesla",
|
||||
"key" => ":adapter",
|
||||
"value" => "Tesla.Adapter.Httpc",
|
||||
"db" => [":adapter"]
|
||||
"group" => ":pleroma",
|
||||
"key" => ":key1",
|
||||
"value" => "Pleroma.Web.Endpoint",
|
||||
"db" => [":key1"]
|
||||
}
|
||||
],
|
||||
"need_reboot" => false
|
||||
|
|
@ -653,14 +657,14 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
admin: admin,
|
||||
token: token
|
||||
} do
|
||||
ueberauth = Application.get_env(:ueberauth, Ueberauth)
|
||||
prometheus = Application.get_env(:prometheus, Pleroma.Web.Endpoint.MetricsExporter)
|
||||
insert(:config, key: :keyaa1)
|
||||
insert(:config, key: :keyaa2)
|
||||
|
||||
config3 =
|
||||
insert(:config,
|
||||
group: :ueberauth,
|
||||
key: Ueberauth
|
||||
group: :prometheus,
|
||||
key: Pleroma.Web.Endpoint.MetricsExporter
|
||||
)
|
||||
|
||||
conn =
|
||||
|
|
@ -693,7 +697,9 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
|
||||
assert Application.get_env(:pleroma, :keyaa1) == "another_value"
|
||||
assert Application.get_env(:pleroma, :keyaa2) == "another_value"
|
||||
assert Application.get_env(:ueberauth, Ueberauth) == config3.value
|
||||
|
||||
assert Application.get_env(:prometheus, Pleroma.Web.Endpoint.MetricsExporter) ==
|
||||
config3.value
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|
|
@ -704,8 +710,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
configs: [
|
||||
%{group: ":pleroma", key: ":keyaa2", delete: true},
|
||||
%{
|
||||
group: ":ueberauth",
|
||||
key: "Ueberauth",
|
||||
group: ":prometheus",
|
||||
key: "Pleroma.Web.Endpoint.MetricsExporter",
|
||||
delete: true
|
||||
}
|
||||
]
|
||||
|
|
@ -716,7 +722,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
"need_reboot" => false
|
||||
}
|
||||
|
||||
assert Application.get_env(:ueberauth, Ueberauth) == ueberauth
|
||||
assert Application.get_env(:prometheus, Pleroma.Web.Endpoint.MetricsExporter) == prometheus
|
||||
refute Keyword.has_key?(Application.get_all_env(:pleroma), :keyaa2)
|
||||
end
|
||||
|
||||
|
|
@ -1021,8 +1027,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
|> post("/api/pleroma/admin/config", %{
|
||||
configs: [
|
||||
%{
|
||||
"group" => ":oban",
|
||||
"key" => ":queues",
|
||||
"group" => ":pleroma",
|
||||
"key" => ":nonexist_oban_queues",
|
||||
"value" => [
|
||||
%{"tuple" => [":federator_incoming", 50]},
|
||||
%{"tuple" => [":federator_outgoing", 50]},
|
||||
|
|
@ -1039,8 +1045,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
assert json_response_and_validate_schema(conn, 200) == %{
|
||||
"configs" => [
|
||||
%{
|
||||
"group" => ":oban",
|
||||
"key" => ":queues",
|
||||
"group" => ":pleroma",
|
||||
"key" => ":nonexist_oban_queues",
|
||||
"value" => [
|
||||
%{"tuple" => [":federator_incoming", 50]},
|
||||
%{"tuple" => [":federator_outgoing", 50]},
|
||||
|
|
@ -1245,6 +1251,57 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
refute ConfigDB.get_by_group_and_key(:pleroma, :database_config_whitelist)
|
||||
end
|
||||
|
||||
test "doesn't set keys in the blacklist", %{conn: conn} do
|
||||
clear_config(:database_config_blacklist, [
|
||||
{:pleroma, :logger},
|
||||
{:pleroma, :key2},
|
||||
{:pleroma, Pleroma.Captcha.NotReal},
|
||||
{:blacklisted_group}
|
||||
])
|
||||
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/pleroma/admin/config", %{
|
||||
configs: [
|
||||
%{group: ":pleroma", key: ":logger", value: "logconf"},
|
||||
%{group: ":pleroma", key: ":key1", value: "value1"},
|
||||
%{group: ":pleroma", key: ":key2", value: "value2"},
|
||||
%{group: ":not_real", key: ":anything", value: "value3"},
|
||||
%{group: ":blacklisted_group", key: ":anything", value: "value4"}
|
||||
]
|
||||
})
|
||||
|
||||
assert Application.get_env(:pleroma, :logger) == nil
|
||||
assert Application.get_env(:pleroma, :key1) == "value1"
|
||||
assert Application.get_env(:pleroma, :key2) == nil
|
||||
assert Application.get_env(:pleroma, Pleroma.Captcha.NotReal) == nil
|
||||
assert Application.get_env(:blacklisted_group, :anything) == nil
|
||||
end
|
||||
|
||||
test "doesn't allow updating the database_config_blacklist itself", %{conn: conn} do
|
||||
original_blacklist = Pleroma.Config.get(:database_config_blacklist)
|
||||
|
||||
refute ConfigDB.get_by_group_and_key(:pleroma, :database_config_blacklist)
|
||||
|
||||
%{"configs" => configs} =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/pleroma/admin/config", %{
|
||||
configs: [
|
||||
%{
|
||||
group: ":pleroma",
|
||||
key: ":database_config_blacklist",
|
||||
value: [%{"tuple" => [":pleroma", ":key1"]}]
|
||||
}
|
||||
]
|
||||
})
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert configs == []
|
||||
assert Pleroma.Config.get(:database_config_blacklist) == original_blacklist
|
||||
refute ConfigDB.get_by_group_and_key(:pleroma, :database_config_blacklist)
|
||||
end
|
||||
|
||||
test "args for Pleroma.Upload.Filter.Mogrify with custom tuples", %{conn: conn} do
|
||||
assert conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|
|
@ -1508,12 +1565,45 @@ 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
|
||||
clear_config(:database_config_whitelist, [
|
||||
{:pleroma, :instance},
|
||||
{:pleroma, :activitypub},
|
||||
{:pleroma, Pleroma.Upload}
|
||||
])
|
||||
|
||||
clear_config(:database_config_blacklist, [
|
||||
{:pleroma, Pleroma.Upload}
|
||||
])
|
||||
|
||||
children =
|
||||
conn
|
||||
|> get("/api/pleroma/admin/config/descriptions")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert length(children) == 2
|
||||
|
||||
assert Enum.count(children, fn c -> c["group"] == ":pleroma" end) == 2
|
||||
|
||||
instance = Enum.find(children, fn c -> c["key"] == ":instance" end)
|
||||
assert instance["children"]
|
||||
|
||||
activitypub = Enum.find(children, fn c -> c["key"] == ":activitypub" end)
|
||||
assert activitypub["children"]
|
||||
|
||||
web_endpoint = Enum.find(children, fn c -> c["key"] == "Pleroma.Upload" end)
|
||||
refute web_endpoint["children"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
59
test/pleroma/web/feed/feed_view_test.exs
Normal file
59
test/pleroma/web/feed/feed_view_test.exs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Feed.FeedViewTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.Web.Feed.FeedView
|
||||
|
||||
test "escapes fallback content" do
|
||||
data = %{
|
||||
"attachment" => [],
|
||||
"content" => "",
|
||||
"summary" => "<script>alert('feed')</script>"
|
||||
}
|
||||
|
||||
expected = "<script>alert('feed')</script>"
|
||||
|
||||
assert FeedView.atom_content_encoded(data) == expected
|
||||
assert FeedView.rss_content_encoded(data) == expected
|
||||
end
|
||||
|
||||
test "does not preview or classify sensitive attachments" do
|
||||
attachment = %{
|
||||
"name" => "spoiler",
|
||||
"url" => [%{"href" => "https://example.com/spoiler.png", "mediaType" => "image/png"}]
|
||||
}
|
||||
|
||||
data = %{
|
||||
"attachment" => [attachment],
|
||||
"content" => "content warning",
|
||||
"sensitive" => true
|
||||
}
|
||||
|
||||
assert FeedView.atom_content_encoded(data) == "content warning"
|
||||
assert FeedView.rss_content_encoded(data) == "content warning"
|
||||
assert FeedView.media_content_xml(attachment, true) == ""
|
||||
assert FeedView.media_content_xml(attachment, "true") == ""
|
||||
|
||||
media_content = FeedView.media_content_xml(attachment, false)
|
||||
assert media_content =~ "<media:description type=\"plain\">spoiler</media:description>"
|
||||
refute media_content =~ "media:rating"
|
||||
|
||||
assert FeedView.media_content_xml(Map.delete(attachment, "name"), false) ==
|
||||
~s(<media:content url="https://example.com/spoiler.png" type="image/png" medium="image"/>\n)
|
||||
end
|
||||
|
||||
test "does not treat a legacy false string as sensitive" do
|
||||
data = %{
|
||||
"attachment" => [
|
||||
%{"url" => [%{"href" => "https://example.com/image.png", "mediaType" => "image/png"}]}
|
||||
],
|
||||
"content" => "visible",
|
||||
"sensitive" => "false"
|
||||
}
|
||||
|
||||
assert FeedView.atom_content_encoded(data) =~ "<img"
|
||||
end
|
||||
end
|
||||
|
|
@ -30,9 +30,18 @@ defmodule Pleroma.Web.Feed.TagControllerTest do
|
|||
%{
|
||||
"url" => [
|
||||
%{
|
||||
"href" =>
|
||||
"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
|
||||
"href" => "https://peertube.moe/video.mp4?first=one&second=two",
|
||||
"mediaType" => "video/mp4",
|
||||
"size" => "123",
|
||||
"type" => "Link"
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
"url" => [
|
||||
%{
|
||||
"href" => "https://peertube.moe/notes.pdf",
|
||||
"mediaType" => "application/pdf",
|
||||
"type" => "Link"
|
||||
}
|
||||
]
|
||||
|
|
@ -63,6 +72,15 @@ defmodule Pleroma.Web.Feed.TagControllerTest do
|
|||
]
|
||||
|
||||
assert xpath(xml, ~x"//feed/entry/author/name/text()"ls) == [user.nickname, user.nickname]
|
||||
assert xpath(xml, ~x"//feed/entry/link[@rel='self']/@href"sl) == []
|
||||
assert length(xpath(xml, ~x"//feed/entry/link[@rel='alternate']/@href"sl)) == 2
|
||||
|
||||
assert xpath(xml, ~x"//feed/entry/link[@rel='enclosure']/@href"sl) == [
|
||||
"https://peertube.moe/video.mp4?first=one&second=two",
|
||||
"https://peertube.moe/notes.pdf"
|
||||
]
|
||||
|
||||
assert xpath(xml, ~x"//feed/entry/link[@rel='enclosure']/@length"sl) == ["123"]
|
||||
|
||||
conn =
|
||||
conn
|
||||
|
|
@ -96,17 +114,28 @@ defmodule Pleroma.Web.Feed.TagControllerTest do
|
|||
%{
|
||||
"url" => [
|
||||
%{
|
||||
"href" =>
|
||||
"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4",
|
||||
"href" => "https://peertube.moe/video.mp4?first=one&second=two",
|
||||
"mediaType" => "video/mp4",
|
||||
"size" => "123",
|
||||
"type" => "Link"
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
"url" => [
|
||||
%{
|
||||
"href" => "https://peertube.moe/notes.pdf",
|
||||
"mediaType" => "application/pdf",
|
||||
"type" => "Link"
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
edited_at = NaiveDateTime.add(object.updated_at, 60)
|
||||
|
||||
object
|
||||
|> Ecto.Changeset.change(data: object_data)
|
||||
|> Ecto.Changeset.change(data: object_data, updated_at: edited_at)
|
||||
|> Pleroma.Repo.update()
|
||||
|
||||
{:ok, activity2} = CommonAPI.post(user, %{status: "42 This is :moominmamma #PleromaArt"})
|
||||
|
|
@ -136,15 +165,27 @@ defmodule Pleroma.Web.Feed.TagControllerTest do
|
|||
~c"yeah #PleromaArt"
|
||||
]
|
||||
|
||||
assert xpath(xml, ~x"//channel/item/guid/@isPermaLink"sl) == ["true", "true"]
|
||||
|
||||
assert xpath(xml, ~x"//channel/item/pubDate/text()"sl) == [
|
||||
FeedView.to_rfc2822(activity2.data["published"]),
|
||||
FeedView.to_rfc2822(activity1.data["published"])
|
||||
]
|
||||
|
||||
assert xpath(xml, ~x"//channel/item/enclosure/@url"sl) == [
|
||||
"https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4"
|
||||
assert xpath(xml, ~x"//channel/lastBuildDate/text()"s) ==
|
||||
FeedView.to_rfc2822(edited_at)
|
||||
|
||||
assert xpath(xml, ~x"//channel/item/media:content/@url"sl) == [
|
||||
"https://peertube.moe/video.mp4?first=one&second=two",
|
||||
"https://peertube.moe/notes.pdf"
|
||||
]
|
||||
|
||||
assert xpath(xml, ~x"//channel/item/enclosure/@url"sl) == [
|
||||
"https://peertube.moe/video.mp4?first=one&second=two"
|
||||
]
|
||||
|
||||
assert xpath(xml, ~x"//channel/item/enclosure/@length"sl) == ["123"]
|
||||
|
||||
obj1 = Object.normalize(activity1, fetch: false)
|
||||
obj2 = Object.normalize(activity2, fetch: false)
|
||||
|
||||
|
|
@ -181,6 +222,90 @@ defmodule Pleroma.Web.Feed.TagControllerTest do
|
|||
]
|
||||
end
|
||||
|
||||
test "does not fabricate lastBuildDate for an empty RSS feed", %{conn: conn} do
|
||||
response =
|
||||
conn
|
||||
|> put_req_header("accept", "application/rss+xml")
|
||||
|> get(tag_feed_path(conn, :feed, "empty.rss"))
|
||||
|> response(200)
|
||||
|
||||
refute response =~ "<lastBuildDate>"
|
||||
end
|
||||
|
||||
test "provides a valid updated timestamp for an empty Atom feed", %{conn: conn} do
|
||||
response =
|
||||
conn
|
||||
|> put_req_header("accept", "application/atom+xml")
|
||||
|> get(tag_feed_path(conn, :feed, "empty.atom"))
|
||||
|> response(200)
|
||||
|
||||
updated = response |> parse() |> xpath(~x"//feed/updated/text()"s)
|
||||
assert {:ok, _datetime, 0} = DateTime.from_iso8601(updated)
|
||||
end
|
||||
|
||||
test "escapes Atom IDs and tag feed self links", %{conn: conn} do
|
||||
user = insert(:user, local: false)
|
||||
object_id = "https://remote.example/object?first=one&second=two"
|
||||
|
||||
note =
|
||||
insert(:note,
|
||||
user: user,
|
||||
object_local: false,
|
||||
data: %{
|
||||
"content" => "remote #atomlink",
|
||||
"external_url" => object_id,
|
||||
"id" => object_id,
|
||||
"summary" => "",
|
||||
"tag" => ["atomlink"]
|
||||
}
|
||||
)
|
||||
|
||||
insert(:note_activity, user: user, note: note, local: false, object_local: false)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> put_req_header("accept", "application/atom+xml")
|
||||
|> get("/tags/atomlink.atom?third=three&fourth=four")
|
||||
|> response(200)
|
||||
|
||||
assert response =~ "https://remote.example/object?first=one&second=two"
|
||||
assert response =~ "third=three&fourth=four"
|
||||
|
||||
xml = parse(response)
|
||||
assert xpath(xml, ~x"//feed/entry/id/text()"s) == object_id
|
||||
end
|
||||
|
||||
test "escapes and round-trips RSS item permalinks", %{conn: conn} do
|
||||
user = insert(:user, local: false)
|
||||
permalink = "https://remote.example/post?first=one&second=two"
|
||||
|
||||
note =
|
||||
insert(:note,
|
||||
user: user,
|
||||
object_local: false,
|
||||
data: %{
|
||||
"content" => "remote #rsslink",
|
||||
"external_url" => permalink,
|
||||
"summary" => "",
|
||||
"tag" => ["rsslink"]
|
||||
}
|
||||
)
|
||||
|
||||
insert(:note_activity, user: user, note: note, local: false, object_local: false)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> put_req_header("accept", "application/rss+xml")
|
||||
|> get(tag_feed_path(conn, :feed, "rsslink.rss"))
|
||||
|> response(200)
|
||||
|
||||
assert response =~ "https://remote.example/post?first=one&second=two"
|
||||
|
||||
xml = parse(response)
|
||||
assert xpath(xml, ~x"//channel/item/guid/text()"s) == permalink
|
||||
assert xpath(xml, ~x"//channel/item/link/text()"s) == permalink
|
||||
end
|
||||
|
||||
describe "private instance" do
|
||||
setup do: clear_config([:instance, :public], false)
|
||||
|
||||
|
|
|
|||
|
|
@ -83,12 +83,12 @@ defmodule Pleroma.Web.Feed.UserControllerTest do
|
|||
|> get(user_feed_path(conn, :feed, user.nickname))
|
||||
|> response(200)
|
||||
|
||||
activity_titles =
|
||||
resp
|
||||
|> SweetXml.parse()
|
||||
|> SweetXml.xpath(~x"//entry/title/text()"l)
|
||||
xml = SweetXml.parse(resp)
|
||||
activity_titles = SweetXml.xpath(xml, ~x"//entry/title/text()"l)
|
||||
|
||||
assert activity_titles == [~c"Won't, didn'...", ~c"2hu", ~c"2hu & as"]
|
||||
assert SweetXml.xpath(xml, ~x"//entry/link[@rel='self']/@href"sl) == []
|
||||
assert length(SweetXml.xpath(xml, ~x"//entry/link[@rel='alternate']/@href"sl)) == 3
|
||||
assert resp =~ FeedView.escape(object.data["content"])
|
||||
assert resp =~ FeedView.escape(object.data["summary"])
|
||||
assert resp =~ FeedView.escape(object.data["context"])
|
||||
|
|
@ -138,6 +138,107 @@ defmodule Pleroma.Web.Feed.UserControllerTest do
|
|||
assert activity_titles == [~c"2hu & as"]
|
||||
end
|
||||
|
||||
test "escapes query parameters in feed self links", %{conn: conn, user: user} do
|
||||
for format <- ~w[atom rss] do
|
||||
response =
|
||||
conn
|
||||
|> get("/users/#{user.nickname}/feed.#{format}?first=one&second=two")
|
||||
|> response(200)
|
||||
|
||||
assert response =~ "first=one&second=two"
|
||||
refute response =~ "first=one&second=two"
|
||||
end
|
||||
end
|
||||
|
||||
test "keeps fallback summary markup inert in rendered Atom content", %{
|
||||
conn: conn,
|
||||
object: object,
|
||||
user: user
|
||||
} do
|
||||
data =
|
||||
object.data
|
||||
|> Map.put("content", "")
|
||||
|> Map.put("summary", "<script>alert('feed')</script>")
|
||||
|
||||
object
|
||||
|> Ecto.Changeset.change(data: data)
|
||||
|> Pleroma.Repo.update!()
|
||||
|
||||
response =
|
||||
conn
|
||||
|> get("/users/#{user.nickname}/feed.atom")
|
||||
|> response(200)
|
||||
|
||||
assert response =~
|
||||
"&lt;script&gt;alert(&#39;feed&#39;)&lt;/script&gt;"
|
||||
|
||||
content_values = response |> parse() |> xpath(~x"//entry/content/text()"sl)
|
||||
|
||||
assert Enum.any?(content_values, fn content ->
|
||||
String.starts_with?(content, "<script>alert('feed')</script>")
|
||||
end)
|
||||
end
|
||||
|
||||
test "does not expose sensitive attachments as enclosures", %{
|
||||
conn: conn,
|
||||
object: object,
|
||||
user: user
|
||||
} do
|
||||
object
|
||||
|> Ecto.Changeset.change(data: Map.put(object.data, "sensitive", true))
|
||||
|> Pleroma.Repo.update!()
|
||||
|
||||
atom_response =
|
||||
conn
|
||||
|> get("/users/#{user.nickname}/feed.atom")
|
||||
|> response(200)
|
||||
|
||||
rss_response =
|
||||
conn
|
||||
|> get("/users/#{user.nickname}/feed.rss")
|
||||
|> response(200)
|
||||
|
||||
refute atom_response =~ "rel=\"enclosure\""
|
||||
refute rss_response =~ "<enclosure "
|
||||
end
|
||||
|
||||
test "uses enclosure fallbacks for user feeds", %{conn: conn, object: object, user: user} do
|
||||
attachment = %{"url" => [%{"href" => "https://example.com/file.bin"}]}
|
||||
|
||||
object
|
||||
|> Ecto.Changeset.change(data: Map.put(object.data, "attachment", [attachment]))
|
||||
|> Pleroma.Repo.update!()
|
||||
|
||||
atom_xml =
|
||||
conn
|
||||
|> get("/users/#{user.nickname}/feed.atom")
|
||||
|> response(200)
|
||||
|> parse()
|
||||
|
||||
assert xpath(atom_xml, ~x"//entry/link[@rel='enclosure']/@href"sl) == [
|
||||
"https://example.com/file.bin"
|
||||
]
|
||||
|
||||
assert xpath(atom_xml, ~x"//entry/link[@rel='enclosure']/@type"sl) == [
|
||||
"application/octet-stream"
|
||||
]
|
||||
|
||||
assert xpath(atom_xml, ~x"//entry/link[@rel='enclosure']/@length"sl) == []
|
||||
|
||||
rss_xml =
|
||||
conn
|
||||
|> get("/users/#{user.nickname}/feed.rss")
|
||||
|> response(200)
|
||||
|> parse()
|
||||
|
||||
assert xpath(rss_xml, ~x"//item/enclosure/@url"sl) == []
|
||||
assert xpath(rss_xml, ~x"//item/media:content/@url"sl) == ["https://example.com/file.bin"]
|
||||
|
||||
assert xpath(rss_xml, ~x"//item/media:content/@type"sl) == [
|
||||
"application/octet-stream"
|
||||
]
|
||||
end
|
||||
|
||||
test "returns 404 for a missing feed", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -19,6 +19,16 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do
|
|||
|> json_response_and_validate_schema(:ok)
|
||||
end
|
||||
|
||||
test "creating a list with an emoji" do
|
||||
%{conn: conn} = oauth_access(["write:lists"])
|
||||
|
||||
assert %{"title" => "cuties", "pleroma" => %{"emoji" => "🕓", "emoji_url" => nil}} =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/v1/lists", %{"title" => "cuties", "emoji" => "🕓"})
|
||||
|> json_response_and_validate_schema(:ok)
|
||||
end
|
||||
|
||||
test "renders error for invalid params" do
|
||||
%{conn: conn} = oauth_access(["write:lists"])
|
||||
|
||||
|
|
@ -31,6 +41,18 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do
|
|||
json_response_and_validate_schema(conn, 400)
|
||||
end
|
||||
|
||||
test "renders error for an invalid emoji" do
|
||||
%{conn: conn} = oauth_access(["write:lists"])
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/v1/lists", %{"title" => "cuties", "emoji" => "not an emoji"})
|
||||
|
||||
assert %{"error" => "Invalid emoji"} =
|
||||
json_response_and_validate_schema(conn, :unprocessable_entity)
|
||||
end
|
||||
|
||||
test "listing a user's lists" do
|
||||
%{conn: conn} = oauth_access(["read:lists", "write:lists"])
|
||||
|
||||
|
|
@ -159,6 +181,17 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do
|
|||
|> json_response_and_validate_schema(:ok)
|
||||
end
|
||||
|
||||
test "updating a list's emoji" do
|
||||
%{user: user, conn: conn} = oauth_access(["write:lists"])
|
||||
{:ok, list} = Pleroma.List.create(%{title: "name"}, user)
|
||||
|
||||
assert %{"pleroma" => %{"emoji" => "🕓", "emoji_url" => nil}} =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> put("/api/v1/lists/#{list.id}", %{"title" => "name", "emoji" => "🕓"})
|
||||
|> json_response_and_validate_schema(:ok)
|
||||
end
|
||||
|
||||
test "validates title when renaming a list" do
|
||||
%{user: user, conn: conn} = oauth_access(["write:lists"])
|
||||
{:ok, list} = Pleroma.List.create(%{title: "name"}, user)
|
||||
|
|
|
|||
|
|
@ -577,6 +577,298 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
|
|||
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
|
||||
end
|
||||
|
||||
describe "GET /api/v2/notifications" do
|
||||
test "groups favourite notifications for the same status" do
|
||||
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||
other_user1 = insert(:user)
|
||||
other_user2 = insert(:user)
|
||||
|
||||
{:ok, status} = CommonAPI.post(user, %{status: "hello"})
|
||||
{:ok, _} = CommonAPI.favorite(status.id, other_user1)
|
||||
{:ok, _} = CommonAPI.favorite(status.id, other_user2)
|
||||
|
||||
notifications = Repo.all(Notification)
|
||||
notification_ids = Enum.map(notifications, &to_string(&1.id))
|
||||
assert [persisted_group_key] = notifications |> Enum.map(& &1.group_key) |> Enum.uniq()
|
||||
assert is_binary(persisted_group_key)
|
||||
|
||||
result =
|
||||
conn
|
||||
|> get("/api/v2/notifications")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert [%{"id" => account_id1}, %{"id" => account_id2}] = result["accounts"]
|
||||
assert account_id1 in [other_user1.id, other_user2.id]
|
||||
assert account_id2 in [other_user1.id, other_user2.id]
|
||||
assert [%{"id" => status_id}] = result["statuses"]
|
||||
assert status_id == status.id
|
||||
|
||||
assert [group] = result["notification_groups"]
|
||||
assert group["type"] == "favourite"
|
||||
assert group["notifications_count"] == 2
|
||||
assert group["status_id"] == status.id
|
||||
assert group["most_recent_notification_id"] in notification_ids
|
||||
assert group["page_min_id"] in notification_ids
|
||||
assert group["page_max_id"] in notification_ids
|
||||
assert group["latest_page_notification_at"]
|
||||
assert Enum.sort(group["sample_account_ids"]) == Enum.sort([other_user1.id, other_user2.id])
|
||||
group_key = group["group_key"]
|
||||
assert group_key == persisted_group_key
|
||||
|
||||
assert [%{"group_key" => ^group_key}, %{"group_key" => ^group_key}] =
|
||||
conn
|
||||
|> get("/api/v1/notifications")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert %{"notification_groups" => [shown_group]} =
|
||||
conn
|
||||
|> get("/api/v2/notifications/#{group_key}")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert shown_group["group_key"] == group_key
|
||||
assert shown_group["notifications_count"] == 2
|
||||
refute Map.has_key?(shown_group, "page_min_id")
|
||||
refute Map.has_key?(shown_group, "page_max_id")
|
||||
refute Map.has_key?(shown_group, "latest_page_notification_at")
|
||||
end
|
||||
|
||||
test "round-trips ungrouped group keys when grouped_types excludes a type" do
|
||||
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, status} = CommonAPI.post(user, %{status: "hello"})
|
||||
{:ok, _} = CommonAPI.favorite(status.id, other_user)
|
||||
|
||||
%{"notification_groups" => [%{"group_key" => "ungrouped-" <> _ = group_key}]} =
|
||||
conn
|
||||
|> get("/api/v2/notifications?grouped_types[]=reblog")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert %{
|
||||
"notification_groups" => [%{"group_key" => ^group_key, "notifications_count" => 1}]
|
||||
} =
|
||||
conn
|
||||
|> get("/api/v2/notifications/#{group_key}")
|
||||
|> json_response_and_validate_schema(200)
|
||||
end
|
||||
|
||||
test "keeps legacy notifications without persisted group keys ungrouped" do
|
||||
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||
other_user1 = insert(:user)
|
||||
other_user2 = insert(:user)
|
||||
|
||||
{:ok, status} = CommonAPI.post(user, %{status: "hello"})
|
||||
{:ok, favorite1} = CommonAPI.favorite(status.id, other_user1)
|
||||
{:ok, favorite2} = CommonAPI.favorite(status.id, other_user2)
|
||||
|
||||
notification_ids =
|
||||
[favorite1, favorite2]
|
||||
|> Enum.map(&get_notification_id_by_activity/1)
|
||||
|
||||
Repo.update_all(Notification, set: [group_key: nil])
|
||||
|
||||
expected_group_keys = Enum.map(notification_ids, &"ungrouped-#{&1}")
|
||||
|
||||
assert [%{"group_key" => group_key1}, %{"group_key" => group_key2}] =
|
||||
conn
|
||||
|> get("/api/v1/notifications")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert Enum.sort([group_key1, group_key2]) == Enum.sort(expected_group_keys)
|
||||
|
||||
%{"notification_groups" => groups} =
|
||||
conn
|
||||
|> get("/api/v2/notifications")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert Enum.sort(Enum.map(groups, & &1["group_key"])) == Enum.sort(expected_group_keys)
|
||||
assert Enum.all?(groups, &(&1["notifications_count"] == 1))
|
||||
|
||||
group_key = List.first(expected_group_keys)
|
||||
|
||||
assert %{
|
||||
"notification_groups" => [%{"group_key" => ^group_key, "notifications_count" => 1}]
|
||||
} =
|
||||
conn
|
||||
|> get("/api/v2/notifications/#{group_key}")
|
||||
|> json_response_and_validate_schema(200)
|
||||
end
|
||||
|
||||
test "paginates notification groups instead of raw notifications" do
|
||||
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||
other_user1 = insert(:user)
|
||||
other_user2 = insert(:user)
|
||||
other_user3 = insert(:user)
|
||||
|
||||
{:ok, older_status} = CommonAPI.post(user, %{status: "older"})
|
||||
{:ok, _} = CommonAPI.favorite(older_status.id, other_user3)
|
||||
|
||||
{:ok, newer_status} = CommonAPI.post(user, %{status: "newer"})
|
||||
{:ok, _} = CommonAPI.favorite(newer_status.id, other_user1)
|
||||
{:ok, _} = CommonAPI.favorite(newer_status.id, other_user2)
|
||||
older_status_id = older_status.id
|
||||
newer_status_id = newer_status.id
|
||||
|
||||
%{"notification_groups" => groups} =
|
||||
conn
|
||||
|> get("/api/v2/notifications?limit=2")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert [
|
||||
%{"status_id" => ^newer_status_id, "notifications_count" => 2},
|
||||
%{"status_id" => ^older_status_id, "notifications_count" => 1}
|
||||
] = groups
|
||||
end
|
||||
|
||||
test "uses group-level cursors when paginating notification groups" do
|
||||
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||
other_user1 = insert(:user)
|
||||
other_user2 = insert(:user)
|
||||
other_user3 = insert(:user)
|
||||
|
||||
{:ok, older_status} = CommonAPI.post(user, %{status: "older"})
|
||||
{:ok, _} = CommonAPI.favorite(older_status.id, other_user3)
|
||||
|
||||
{:ok, newer_status} = CommonAPI.post(user, %{status: "newer"})
|
||||
{:ok, _} = CommonAPI.favorite(newer_status.id, other_user1)
|
||||
{:ok, _} = CommonAPI.favorite(newer_status.id, other_user2)
|
||||
older_status_id = older_status.id
|
||||
newer_status_id = newer_status.id
|
||||
|
||||
assert %{
|
||||
"notification_groups" => [
|
||||
%{
|
||||
"status_id" => ^newer_status_id,
|
||||
"notifications_count" => 2,
|
||||
"most_recent_notification_id" => cursor
|
||||
}
|
||||
]
|
||||
} =
|
||||
conn
|
||||
|> get("/api/v2/notifications?limit=1")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert %{
|
||||
"notification_groups" => [
|
||||
%{"status_id" => ^older_status_id, "notifications_count" => 1}
|
||||
]
|
||||
} =
|
||||
conn
|
||||
|> get("/api/v2/notifications?limit=1&max_id=#{cursor}")
|
||||
|> json_response_and_validate_schema(200)
|
||||
end
|
||||
|
||||
test "returns total notification count for a partially represented group" do
|
||||
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||
other_user1 = insert(:user)
|
||||
other_user2 = insert(:user)
|
||||
other_user3 = insert(:user)
|
||||
|
||||
{:ok, grouped_status} = CommonAPI.post(user, %{status: "grouped"})
|
||||
{:ok, _} = CommonAPI.favorite(grouped_status.id, other_user1)
|
||||
|
||||
{:ok, other_status} = CommonAPI.post(user, %{status: "other"})
|
||||
{:ok, _} = CommonAPI.favorite(other_status.id, other_user3)
|
||||
{:ok, _} = CommonAPI.favorite(grouped_status.id, other_user2)
|
||||
grouped_status_id = grouped_status.id
|
||||
|
||||
assert %{
|
||||
"notification_groups" => [
|
||||
%{"status_id" => ^grouped_status_id, "notifications_count" => 2}
|
||||
]
|
||||
} =
|
||||
conn
|
||||
|> get("/api/v2/notifications?limit=1")
|
||||
|> json_response_and_validate_schema(200)
|
||||
end
|
||||
|
||||
test "counts unread notification groups" do
|
||||
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||
other_user1 = insert(:user)
|
||||
other_user2 = insert(:user)
|
||||
mentioner = insert(:user)
|
||||
|
||||
{:ok, status} = CommonAPI.post(user, %{status: "hello"})
|
||||
{:ok, _} = CommonAPI.favorite(status.id, other_user1)
|
||||
{:ok, _} = CommonAPI.favorite(status.id, other_user2)
|
||||
{:ok, _} = CommonAPI.post(mentioner, %{status: "hi @#{user.nickname}"})
|
||||
|
||||
assert %{"count" => 2} =
|
||||
conn
|
||||
|> get("/api/v2/notifications/unread_count")
|
||||
|> json_response_and_validate_schema(200)
|
||||
end
|
||||
|
||||
test "counts unread notification groups newer than the notification marker" do
|
||||
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||
other_user = insert(:user)
|
||||
mentioner = insert(:user)
|
||||
|
||||
{:ok, status} = CommonAPI.post(user, %{status: "older"})
|
||||
{:ok, favorite_activity} = CommonAPI.favorite(status.id, other_user)
|
||||
marker_id = get_notification_id_by_activity(favorite_activity)
|
||||
|
||||
{:ok, _} = CommonAPI.post(mentioner, %{status: "newer @#{user.nickname}"})
|
||||
|
||||
{:ok, _} =
|
||||
Pleroma.Marker.upsert(user, %{"notifications" => %{"last_read_id" => marker_id}})
|
||||
|
||||
assert %{"count" => 1} =
|
||||
conn
|
||||
|> get("/api/v2/notifications/unread_count")
|
||||
|> json_response_and_validate_schema(200)
|
||||
end
|
||||
|
||||
test "does not preserve stale cursor params in grouped notification link headers" do
|
||||
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, activity1} = CommonAPI.post(other_user, %{status: "one @#{user.nickname}"})
|
||||
{:ok, _activity2} = CommonAPI.post(other_user, %{status: "two @#{user.nickname}"})
|
||||
{:ok, activity3} = CommonAPI.post(other_user, %{status: "three @#{user.nickname}"})
|
||||
|
||||
notification1_id = get_notification_id_by_activity(activity1)
|
||||
notification3_id = get_notification_id_by_activity(activity3)
|
||||
|
||||
conn = get(conn, "/api/v2/notifications?since_id=#{notification1_id}&limit=1")
|
||||
|
||||
assert [link_header] = get_resp_header(conn, "link")
|
||||
assert link_header =~ ~r/max_id=#{notification3_id}/
|
||||
refute link_header =~ "since_id="
|
||||
end
|
||||
|
||||
test "lists accounts from a notification group" do
|
||||
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||
|
||||
{other_users, %{"group_key" => group_key, "sample_account_ids" => sample_account_ids}} =
|
||||
create_favourite_notification_group(user, conn)
|
||||
|
||||
assert length(sample_account_ids) == 8
|
||||
|
||||
account_ids =
|
||||
conn
|
||||
|> get("/api/v2/notifications/#{group_key}/accounts")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|> Enum.map(& &1["id"])
|
||||
|
||||
assert Enum.sort(account_ids) == Enum.sort(Enum.map(other_users, & &1.id))
|
||||
end
|
||||
|
||||
test "dismisses a notification group" do
|
||||
%{user: user, conn: conn} = oauth_access(["read:notifications", "write:notifications"])
|
||||
|
||||
{_other_users, %{"group_key" => group_key}} =
|
||||
create_favourite_notification_group(user, conn)
|
||||
|
||||
assert %{} =
|
||||
conn
|
||||
|> post("/api/v2/notifications/#{group_key}/dismiss")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert [] = Notification.for_user(user)
|
||||
end
|
||||
end
|
||||
|
||||
test "doesn't see notifications after muting user with notifications" do
|
||||
%{user: user, conn: conn} = oauth_access(["read:notifications"])
|
||||
user2 = insert(:user)
|
||||
|
|
@ -708,6 +1000,29 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
|
|||
|> to_string()
|
||||
end
|
||||
|
||||
defp create_favourite_notification_group(user, conn) do
|
||||
other_users = insert_list(9, :user)
|
||||
|
||||
{:ok, status} = CommonAPI.post(user, %{status: "hello"})
|
||||
|
||||
Enum.each(other_users, fn other_user ->
|
||||
{:ok, _} = CommonAPI.favorite(status.id, other_user)
|
||||
end)
|
||||
|
||||
%{
|
||||
"notification_groups" => [
|
||||
%{
|
||||
"notifications_count" => 9
|
||||
} = group
|
||||
]
|
||||
} =
|
||||
conn
|
||||
|> get("/api/v2/notifications")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
{other_users, group}
|
||||
end
|
||||
|
||||
defp params_to_query(%{} = params) do
|
||||
Enum.map_join(params, "&", fn
|
||||
{k, v} when is_list(v) -> Enum.map_join(v, "&", &"#{k}[]=#{&1}")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -907,21 +907,53 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
|
|||
{:ok, activity_test1} = CommonAPI.post(user, %{status: "#test #test1"})
|
||||
{:ok, activity_none} = CommonAPI.post(user, %{status: "#test #none"})
|
||||
|
||||
any_test = get(conn, "/api/v1/timelines/tag/test?any[]=test1")
|
||||
for hashtag_timeline_mode <- [:enabled, :disabled] do
|
||||
clear_config([:features, :improved_hashtag_timeline], hashtag_timeline_mode)
|
||||
|
||||
[status_none, status_test1, status_test] = json_response_and_validate_schema(any_test, :ok)
|
||||
any_test = get(conn, "/api/v1/timelines/tag/test?any[]=test1")
|
||||
|
||||
assert to_string(activity_test.id) == status_test["id"]
|
||||
assert to_string(activity_test1.id) == status_test1["id"]
|
||||
assert to_string(activity_none.id) == status_none["id"]
|
||||
[status_none, status_test1, status_test] =
|
||||
json_response_and_validate_schema(any_test, :ok)
|
||||
|
||||
restricted_test = get(conn, "/api/v1/timelines/tag/test?all[]=test1&none[]=none")
|
||||
assert to_string(activity_test.id) == status_test["id"]
|
||||
assert to_string(activity_test1.id) == status_test1["id"]
|
||||
assert to_string(activity_none.id) == status_none["id"]
|
||||
|
||||
assert [status_test1] == json_response_and_validate_schema(restricted_test, :ok)
|
||||
restricted_test = get(conn, "/api/v1/timelines/tag/test?all[]=test1&none[]=none")
|
||||
|
||||
all_test = get(conn, "/api/v1/timelines/tag/test?all[]=none")
|
||||
assert [status_test1] == json_response_and_validate_schema(restricted_test, :ok)
|
||||
|
||||
assert [status_none] == json_response_and_validate_schema(all_test, :ok)
|
||||
all_test = get(conn, "/api/v1/timelines/tag/test?all[]=none")
|
||||
|
||||
assert [status_none] == json_response_and_validate_schema(all_test, :ok)
|
||||
end
|
||||
end
|
||||
|
||||
test "multi-hashtag timeline pagination returns unique statuses", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, activity_one} = CommonAPI.post(user, %{status: "#test #test1"})
|
||||
{:ok, activity_two} = CommonAPI.post(user, %{status: "#test #test1"})
|
||||
{:ok, activity_three} = CommonAPI.post(user, %{status: "#test #test1"})
|
||||
|
||||
for hashtag_timeline_mode <- [:enabled, :disabled] do
|
||||
clear_config([:features, :improved_hashtag_timeline], hashtag_timeline_mode)
|
||||
|
||||
first_page =
|
||||
conn
|
||||
|> get("/api/v1/timelines/tag/test?any[]=test1&limit=2")
|
||||
|> json_response_and_validate_schema(:ok)
|
||||
|
||||
assert Enum.map(first_page, & &1["id"]) ==
|
||||
Enum.map([activity_three, activity_two], &to_string(&1.id))
|
||||
|
||||
second_page =
|
||||
conn
|
||||
|> get("/api/v1/timelines/tag/test?any[]=test1&limit=2&max_id=#{activity_two.id}")
|
||||
|> json_response_and_validate_schema(:ok)
|
||||
|
||||
assert Enum.map(second_page, & &1["id"]) == [to_string(activity_one.id)]
|
||||
end
|
||||
end
|
||||
|
||||
test "muted emotions", %{conn: conn} do
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
702
test/pleroma/web/mastodon_api/views/instance_view_test.exs
Normal file
702
test/pleroma/web/mastodon_api/views/instance_view_test.exs
Normal file
|
|
@ -0,0 +1,702 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.InstanceViewTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
alias Pleroma.Language.LanguageDetectorMock
|
||||
alias Pleroma.StaticStubbedConfigMock
|
||||
alias Pleroma.Web.MastodonAPI.InstanceView
|
||||
|
||||
import Mox
|
||||
|
||||
@common_information_keys [
|
||||
:languages,
|
||||
:rules,
|
||||
:title,
|
||||
:version
|
||||
]
|
||||
|
||||
# Used for filtering out values from configuration.json render in configuration2.json render
|
||||
@configuration2_filter [
|
||||
{:accounts,
|
||||
[
|
||||
:max_pinned_statuses,
|
||||
:max_profile_fields,
|
||||
:profile_field_name_limit,
|
||||
:profile_field_value_limit
|
||||
]},
|
||||
{:statuses, [:characters_reserved_per_url]},
|
||||
{:urls, [:streaming, :status]},
|
||||
{:vapid, [:public_key]},
|
||||
{:translation, [:enabled]},
|
||||
{:timelines_access, [:live_feeds, :hashtag_feeds, :trending_link_feeds]}
|
||||
]
|
||||
|
||||
@pleroma_configuration_filter [{:metadata, [:features, :federation]}]
|
||||
|
||||
@pleroma_configuration2_filter [
|
||||
{:metadata,
|
||||
[
|
||||
:avatar_upload_limit,
|
||||
:background_upload_limit,
|
||||
:banner_upload_limit,
|
||||
:background_image,
|
||||
:chat_limit,
|
||||
:description_limit,
|
||||
:shout_limit
|
||||
]}
|
||||
]
|
||||
|
||||
@show_keys @common_information_keys ++
|
||||
[
|
||||
:uri,
|
||||
:description,
|
||||
:short_description,
|
||||
:email,
|
||||
:urls,
|
||||
:stats,
|
||||
:thumbnail,
|
||||
:registrations,
|
||||
:approval_required,
|
||||
:contact_account,
|
||||
:configuration,
|
||||
# Extra (not present in Mastodon)
|
||||
:max_toot_chars,
|
||||
:max_media_attachments,
|
||||
:poll_limits,
|
||||
:upload_limit,
|
||||
:avatar_upload_limit,
|
||||
:background_upload_limit,
|
||||
:banner_upload_limit,
|
||||
:background_image,
|
||||
:shout_limit,
|
||||
:description_limit,
|
||||
:chat_limit,
|
||||
:pleroma
|
||||
]
|
||||
|
||||
# Checked in other check_* functions
|
||||
@show_filter @common_information_keys ++ [:contact_account, :configuration, :pleroma]
|
||||
|
||||
@show2_keys @common_information_keys ++
|
||||
[
|
||||
:domain,
|
||||
:source_url,
|
||||
:description,
|
||||
:usage,
|
||||
:thumbnail,
|
||||
:configuration,
|
||||
:registrations,
|
||||
:contact,
|
||||
# Extra (not present in Mastodon)
|
||||
:pleroma
|
||||
]
|
||||
|
||||
@show2_filter @common_information_keys ++
|
||||
[:thumbnail, :configuration, :registrations, :contact, :pleroma]
|
||||
|
||||
@features [
|
||||
"pleroma_api",
|
||||
"mastodon_api",
|
||||
"mastodon_api_grouped_notifications",
|
||||
"mastodon_api_streaming",
|
||||
"polls",
|
||||
"v2_suggestions",
|
||||
"pleroma_explicit_addressing",
|
||||
"shareable_emoji_packs",
|
||||
"multifetch",
|
||||
"pleroma:api/v1/notifications:include_types_filter",
|
||||
"editing",
|
||||
"quote_posting",
|
||||
"pleroma_emoji_reactions",
|
||||
"pleroma_custom_emoji_reactions",
|
||||
"pleroma:get:main/ostatus",
|
||||
"pleroma:group_actors",
|
||||
"pleroma:bookmark_folders",
|
||||
"pleroma:block_expiration"
|
||||
]
|
||||
|
||||
@configurable_features [
|
||||
"blockers_visible",
|
||||
"media_proxy",
|
||||
"gopher",
|
||||
"chat",
|
||||
"shout",
|
||||
"pleroma_chat_messages",
|
||||
"pleroma:pin_chats",
|
||||
"relay",
|
||||
"safe_dm_mentions",
|
||||
"exposable_reactions",
|
||||
"profile_directory",
|
||||
"pleroma:language_detection"
|
||||
]
|
||||
@configurable_features_flags [
|
||||
[:activitypub, :blockers_visible],
|
||||
[:media_proxy, :enabled],
|
||||
[:gopher, :enabled],
|
||||
[:shout, :enabled],
|
||||
[Pleroma.Chat, :enabled],
|
||||
[:instance, :allow_relay],
|
||||
[:instance, :safe_dm_mentions],
|
||||
[:instance, :show_reactions],
|
||||
[:instance, :profile_directory]
|
||||
]
|
||||
|
||||
# Filters out values from one layer deep nested Maps.
|
||||
# Filtering types:
|
||||
# - :inclusive -> filters out everything that is not in the filters
|
||||
# - :discriminative -> filters out what is in the filters
|
||||
defp filter_render(input, filter, type, acc)
|
||||
|
||||
defp filter_render(_input_rest, [], type, acc) when type == :inclusive, do: acc
|
||||
|
||||
defp filter_render(input_rest, [], type, acc) when type == :discriminative,
|
||||
do: Map.merge(input_rest, acc)
|
||||
|
||||
defp filter_render(input, filter, type, acc) do
|
||||
{{filter_key, filters}, filter_rest} = List.pop_at(filter, 0)
|
||||
{config_key_values, input_rest} = Map.pop!(input, filter_key)
|
||||
|
||||
filtered =
|
||||
case type do
|
||||
:inclusive ->
|
||||
Map.reject(config_key_values, fn {key, _} -> key not in filters end)
|
||||
|
||||
:discriminative ->
|
||||
Map.reject(config_key_values, fn {key, _} -> key in filters end)
|
||||
end
|
||||
|
||||
filter_render(input_rest, filter_rest, type, Map.put(acc, filter_key, filtered))
|
||||
end
|
||||
|
||||
defp check_common_information(info) do
|
||||
filtered_info = Map.reject(info, fn {key, _value} -> key not in @common_information_keys end)
|
||||
|
||||
expected = %{
|
||||
languages: ["en"],
|
||||
rules: InstanceView.render("rules.json", %{}),
|
||||
title: "Pleroma",
|
||||
version:
|
||||
InstanceView.mastodon_api_level() <>
|
||||
" (compatible; #{Pleroma.Application.named_version()})"
|
||||
}
|
||||
|
||||
Map.equal?(expected, filtered_info)
|
||||
end
|
||||
|
||||
defp check_thumbnail2(info) do
|
||||
thumbnail = info[:thumbnail]
|
||||
|
||||
expected = %{
|
||||
url:
|
||||
URI.merge(Pleroma.Web.Endpoint.url(), "http://localhost:4001/instance/thumbnail.jpeg")
|
||||
|> to_string
|
||||
}
|
||||
|
||||
expected == thumbnail
|
||||
end
|
||||
|
||||
defp check_contact(info, user) do
|
||||
contact = info[:contact]
|
||||
|
||||
expected = %{
|
||||
email: "admin@example.com",
|
||||
account: Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user, for: nil})
|
||||
}
|
||||
|
||||
Map.equal?(expected, contact)
|
||||
end
|
||||
|
||||
defp check_contact_account(info, user) do
|
||||
contact = info[:contact_account]
|
||||
view = Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user, for: nil})
|
||||
|
||||
contact == view
|
||||
end
|
||||
|
||||
defp check_configuration(info) do
|
||||
configuration = info[:configuration]
|
||||
|
||||
expected = %{
|
||||
accounts: %{
|
||||
max_featured_tags: 0
|
||||
},
|
||||
statuses: %{
|
||||
max_characters: 5_000,
|
||||
max_media_attachments: 1_000
|
||||
},
|
||||
media_attachments: %{
|
||||
image_size_limit: 16_000_000,
|
||||
video_size_limit: 16_000_000,
|
||||
supported_mime_types: ["application/octet-stream"]
|
||||
},
|
||||
polls: %{
|
||||
max_options: 20,
|
||||
max_characters_per_option: 200,
|
||||
min_expiration: 0,
|
||||
max_expiration: 365 * 24 * 60 * 60
|
||||
}
|
||||
}
|
||||
|
||||
Map.equal?(expected, configuration)
|
||||
end
|
||||
|
||||
defp check_configuration2(info) do
|
||||
configuration =
|
||||
info[:configuration]
|
||||
|> filter_render(@configuration2_filter, :inclusive, %{})
|
||||
|
||||
expected = %{
|
||||
accounts: %{
|
||||
max_pinned_statuses: 1,
|
||||
max_profile_fields: 10,
|
||||
profile_field_name_limit: 512,
|
||||
profile_field_value_limit: 2048
|
||||
},
|
||||
statuses: %{
|
||||
characters_reserved_per_url: 0
|
||||
},
|
||||
urls: %{
|
||||
streaming: Pleroma.Web.Endpoint.websocket_url(),
|
||||
status: nil
|
||||
},
|
||||
vapid: %{
|
||||
public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
|
||||
},
|
||||
translation: %{enabled: true},
|
||||
timelines_access: %{
|
||||
live_feeds: %{local: "public", remote: "public"},
|
||||
hashtag_feeds: %{local: "public", remote: "public"},
|
||||
# not implemented in Pleroma
|
||||
trending_link_feeds: %{
|
||||
local: "disabled",
|
||||
remote: "disabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map.equal?(expected, configuration)
|
||||
end
|
||||
|
||||
defp check_registrations2(info) do
|
||||
registrations = info[:registrations]
|
||||
|
||||
expected = %{
|
||||
enabled: true,
|
||||
approval_required: false,
|
||||
message: nil,
|
||||
url: nil
|
||||
}
|
||||
|
||||
Map.equal?(expected, registrations)
|
||||
end
|
||||
|
||||
defp check_pleroma_configuration(info) do
|
||||
configuration =
|
||||
info[:pleroma]
|
||||
|> filter_render(@pleroma_configuration_filter, :discriminative, %{})
|
||||
|
||||
expected = %{
|
||||
metadata: %{
|
||||
account_activation_required: false,
|
||||
fields_limits: %{
|
||||
max_fields: 10,
|
||||
max_remote_fields: 20,
|
||||
name_length: 512,
|
||||
value_length: 2048
|
||||
},
|
||||
post_formats: [
|
||||
"text/plain",
|
||||
"text/html",
|
||||
"text/markdown",
|
||||
"text/bbcode",
|
||||
"text/x.misskeymarkdown"
|
||||
],
|
||||
birthday_required: false,
|
||||
birthday_min_age: 0,
|
||||
translation: %{
|
||||
source_languages: ["en", "pl"],
|
||||
target_languages: ["en", "pl"]
|
||||
},
|
||||
base_urls: %{},
|
||||
markup: %{
|
||||
allow_inline_images: true,
|
||||
allow_headings: false,
|
||||
allow_tables: false
|
||||
}
|
||||
},
|
||||
stats: %{mau: Pleroma.User.active_user_count()},
|
||||
vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
|
||||
}
|
||||
|
||||
Map.equal?(expected, configuration)
|
||||
end
|
||||
|
||||
defp check_pleroma_configuration2(info) do
|
||||
configuration =
|
||||
info[:pleroma]
|
||||
|> filter_render(@pleroma_configuration2_filter, :inclusive, %{})
|
||||
|
||||
expected = %{
|
||||
metadata: %{
|
||||
avatar_upload_limit: 2_000_000,
|
||||
background_upload_limit: 4_000_000,
|
||||
banner_upload_limit: 4_000_000,
|
||||
background_image: Pleroma.Web.Endpoint.url() <> "/images/city.jpg",
|
||||
chat_limit: 5_000,
|
||||
description_limit: 5_000,
|
||||
shout_limit: 5_000
|
||||
}
|
||||
}
|
||||
|
||||
Map.equal?(expected, configuration)
|
||||
end
|
||||
|
||||
defp check_show(info) do
|
||||
filter = @show_filter
|
||||
filtered_info = Map.reject(info, fn {key, _value} -> key in filter end)
|
||||
|
||||
expected = %{
|
||||
uri: Pleroma.Web.WebFinger.host(),
|
||||
description: "Pleroma: An efficient and flexible fediverse server",
|
||||
short_description: "",
|
||||
email: "admin@example.com",
|
||||
urls: %{
|
||||
streaming_api: Pleroma.Web.Endpoint.websocket_url()
|
||||
},
|
||||
stats: Pleroma.Stats.get_stats(),
|
||||
thumbnail: "http://localhost:4001/instance/thumbnail.jpeg",
|
||||
registrations: true,
|
||||
approval_required: false,
|
||||
max_toot_chars: 5_000,
|
||||
max_media_attachments: 1_000,
|
||||
poll_limits: %{
|
||||
max_options: 20,
|
||||
max_option_chars: 200,
|
||||
min_expiration: 0,
|
||||
max_expiration: 365 * 24 * 60 * 60
|
||||
},
|
||||
upload_limit: 16_000_000,
|
||||
avatar_upload_limit: 2_000_000,
|
||||
background_upload_limit: 4_000_000,
|
||||
banner_upload_limit: 4_000_000,
|
||||
background_image: Pleroma.Web.Endpoint.url() <> "/images/city.jpg",
|
||||
shout_limit: 5_000,
|
||||
description_limit: 5_000,
|
||||
chat_limit: 5_000
|
||||
}
|
||||
|
||||
Map.equal?(expected, filtered_info)
|
||||
end
|
||||
|
||||
defp check_show2(info) do
|
||||
filter = @show2_filter
|
||||
filtered_info = Map.reject(info, fn {key, _value} -> key in filter end)
|
||||
|
||||
expected = %{
|
||||
domain: Pleroma.Web.WebFinger.host(),
|
||||
source_url: Pleroma.Application.repository(),
|
||||
description: "",
|
||||
usage: %{users: %{active_month: Pleroma.User.active_user_count()}}
|
||||
}
|
||||
|
||||
Map.equal?(expected, filtered_info)
|
||||
end
|
||||
|
||||
# When this fails, a new feature flag was probably added. Add it to the macros above.
|
||||
describe "uncofigurable features" do
|
||||
setup do: Enum.each(@configurable_features_flags, &clear_config(&1, false))
|
||||
|
||||
test "returns always enabled features" do
|
||||
features = @features
|
||||
|
||||
assert Enum.sort(InstanceView.features()) == Enum.sort(features)
|
||||
end
|
||||
end
|
||||
|
||||
describe "all features" do
|
||||
setup do
|
||||
Enum.each(@configurable_features_flags, &clear_config(&1, true))
|
||||
|
||||
# Stub the StaticStubbedConfigMock to return our mock for the provider
|
||||
StaticStubbedConfigMock
|
||||
|> stub(:get, fn
|
||||
[Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock
|
||||
_other -> nil
|
||||
end)
|
||||
|
||||
# Stub the LanguageDetectorMock with default implementations
|
||||
LanguageDetectorMock
|
||||
|> stub(:missing_dependencies, fn -> [] end)
|
||||
|> stub(:configured?, fn -> true end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "returns all features including configurable ones" do
|
||||
features = @features ++ @configurable_features
|
||||
|
||||
assert Enum.sort(InstanceView.features()) == Enum.sort(features)
|
||||
end
|
||||
end
|
||||
|
||||
describe "chat features" do
|
||||
test "advertises Shout without Chats" do
|
||||
clear_config([:shout, :enabled], true)
|
||||
clear_config([Pleroma.Chat, :enabled], false)
|
||||
|
||||
features = InstanceView.features()
|
||||
|
||||
assert "chat" in features
|
||||
assert "shout" in features
|
||||
refute "pleroma_chat_messages" in features
|
||||
refute "pleroma:pin_chats" in features
|
||||
end
|
||||
|
||||
test "advertises Chats without Shout" do
|
||||
clear_config([:shout, :enabled], false)
|
||||
clear_config([Pleroma.Chat, :enabled], true)
|
||||
|
||||
features = InstanceView.features()
|
||||
|
||||
refute "chat" in features
|
||||
refute "shout" in features
|
||||
assert "pleroma_chat_messages" in features
|
||||
assert "pleroma:pin_chats" in features
|
||||
end
|
||||
end
|
||||
|
||||
describe "federation/0" do
|
||||
setup do: clear_config([:mrf, :transparency], true)
|
||||
|
||||
test "quarantined instances" do
|
||||
clear_config([:instance, :quarantined_instances], [{"quarantine.example.com", "crimes"}])
|
||||
|
||||
output = InstanceView.federation()
|
||||
assert %{quarantined_instances: ["quarantine.example.com"]} = output
|
||||
|
||||
assert %{
|
||||
quarantined_instances_info: %{
|
||||
"quarantined_instances" => %{"quarantine.example.com" => %{"reason" => "crimes"}}
|
||||
}
|
||||
} = output
|
||||
end
|
||||
|
||||
test "rejected instances" do
|
||||
clear_config([:instance, :rejected_instances], [
|
||||
{"rejected.example.com", "not enough #cofe posting"}
|
||||
])
|
||||
|
||||
output = InstanceView.federation()
|
||||
|
||||
assert %{
|
||||
rejected_instances: %{
|
||||
"rejected.example.com" => %{"reason" => "not enough #cofe posting"}
|
||||
}
|
||||
} = output
|
||||
end
|
||||
|
||||
test "federating" do
|
||||
clear_config([:instance, :federating], true)
|
||||
|
||||
output = InstanceView.federation()
|
||||
assert %{enabled: true} = output
|
||||
end
|
||||
|
||||
test "transparency" do
|
||||
clear_config([:instance, :quarantined_instances], [{"quarantine.example.com", "crimes"}])
|
||||
clear_config([:instance, :federating], true)
|
||||
clear_config([:mrf, :transparency], false)
|
||||
|
||||
clear_config([:instance, :rejected_instances], [
|
||||
{"rejected.example.com", "not enough #cofe posting"}
|
||||
])
|
||||
|
||||
output = InstanceView.federation()
|
||||
assert %{enabled: true} == output
|
||||
end
|
||||
|
||||
test "MRFs" do
|
||||
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
|
||||
clear_config([:mrf_simple, :reject], [{"rejected.example.com", "not enough #cofe posting"}])
|
||||
|
||||
clear_config([:mrf, :transparency_exclusions], [
|
||||
{"media1.example.com", "the Fediverse doesn't need to know"}
|
||||
])
|
||||
|
||||
clear_config([:mrf_simple, :media_removal], [
|
||||
{"media1.example.com", "NSFW"},
|
||||
{"media2.example.com", "usual suspects"}
|
||||
])
|
||||
|
||||
output = InstanceView.federation()
|
||||
|
||||
expected = %{
|
||||
mrf_simple: %{
|
||||
reject: ["rejected.example.com"],
|
||||
media_removal: ["media2.example.com"]
|
||||
},
|
||||
mrf_hashtag: %{
|
||||
sensitive: ["nsfw"]
|
||||
},
|
||||
exclusions: true,
|
||||
mrf_policies: ["SimplePolicy", "HashtagPolicy"],
|
||||
mrf_simple_info: %{
|
||||
reject: %{
|
||||
"rejected.example.com" => %{"reason" => "not enough #cofe posting"}
|
||||
},
|
||||
media_removal: %{"media2.example.com" => %{"reason" => "usual suspects"}}
|
||||
}
|
||||
}
|
||||
|
||||
assert expected.mrf_simple.reject == output.mrf_simple.reject
|
||||
assert expected.mrf_simple.media_removal == output.mrf_simple.media_removal
|
||||
assert expected.mrf_simple_info == output.mrf_simple_info
|
||||
assert expected.exclusions == output.exclusions
|
||||
end
|
||||
end
|
||||
|
||||
describe "render show.json/show2.json" do
|
||||
setup do
|
||||
Pleroma.Rule.create(%{text: "rule 1"})
|
||||
Pleroma.Rule.create(%{text: "rule 2"})
|
||||
|
||||
user = insert(:user)
|
||||
|
||||
# contact in show2.json
|
||||
clear_config([:instance, :contact_username], user.nickname)
|
||||
|
||||
# Pleroma.Language.Translation.configured?() -> true
|
||||
clear_config([Pleroma.Language.Translation, :provider], TranslationMock)
|
||||
|
||||
# Deprecated, but still used in view
|
||||
clear_config([:instance, :chat_limit], 5_000)
|
||||
|
||||
%{user: user}
|
||||
end
|
||||
|
||||
test "renders show.json properly", %{user: user} do
|
||||
output = InstanceView.render("show.json", %{})
|
||||
expected_keys = Enum.sort(@show_keys)
|
||||
|
||||
assert check_common_information(output)
|
||||
assert check_show(output)
|
||||
assert check_contact_account(output, user)
|
||||
assert check_configuration(output)
|
||||
assert check_pleroma_configuration(output)
|
||||
assert expected_keys == Enum.sort(Map.keys(output))
|
||||
end
|
||||
|
||||
test "renders show2.json properly", %{user: user} do
|
||||
output = InstanceView.render("show2.json", %{})
|
||||
expected_keys = Enum.sort(@show2_keys)
|
||||
|
||||
assert check_common_information(output)
|
||||
assert check_show2(output)
|
||||
assert check_thumbnail2(output)
|
||||
assert check_configuration2(output)
|
||||
assert check_registrations2(output)
|
||||
assert check_contact(output, user)
|
||||
assert check_pleroma_configuration2(output)
|
||||
assert expected_keys == Enum.sort(Map.keys(output))
|
||||
end
|
||||
end
|
||||
|
||||
describe "render rule.json" do
|
||||
setup do
|
||||
rule = Pleroma.Rule.create(%{text: "hambaga"})
|
||||
|
||||
%{rule: rule}
|
||||
end
|
||||
|
||||
test "renders properly", %{rule: rule} do
|
||||
output = InstanceView.render("rule.json", %{rule: rule})
|
||||
|
||||
assert %{text: "hambaga"} = output
|
||||
end
|
||||
end
|
||||
|
||||
describe "render rules.json" do
|
||||
setup do
|
||||
Pleroma.Rule.create(%{text: "rule 1"})
|
||||
Pleroma.Rule.create(%{text: "rule 2"})
|
||||
Pleroma.Rule.create(%{text: "rule 3"})
|
||||
Pleroma.Rule.create(%{text: "rule 4"})
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "renders properly" do
|
||||
output = InstanceView.render("rules.json", %{})
|
||||
keys = Enum.map(output, fn %{text: text} -> text end)
|
||||
|
||||
assert length(keys) == 4
|
||||
assert Enum.all?(output, fn %{text: "rule " <> num} -> String.to_integer(num) end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "render domain_blocks.json" do
|
||||
setup do
|
||||
clear_config([:mrf, :transparency], true)
|
||||
clear_config([:mrf_simple, :media_removal], [{"media1.example.com", "NSFW"}])
|
||||
|
||||
clear_config([:mrf, :transparency_exclusions], [
|
||||
{"removed1.example.com", "the Fediverse doesn't need to know"}
|
||||
])
|
||||
|
||||
clear_config([:mrf_simple, :reject], [
|
||||
{"rejected.example.com", "not enough #cofe posting"},
|
||||
{"rejected-without-reason.example.com", ""}
|
||||
])
|
||||
|
||||
clear_config([:mrf_simple, :federated_timeline_removal], [
|
||||
{"removed1.example.com", "spam"},
|
||||
{"removed2.example.com", "more spam"}
|
||||
])
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "renders properly" do
|
||||
output = InstanceView.render("domain_blocks.json", %{})
|
||||
|
||||
# Media removal is not in @block_severities in the view.
|
||||
# NOTE: Order of these two matters!
|
||||
expected = [
|
||||
%{
|
||||
domain: "removed2.example.com",
|
||||
comment: "more spam",
|
||||
severity: "silence",
|
||||
digest: "5f6649cfc780fac5172dbbcc8a552953f6621a14368d82728b6e8faf51295dc0"
|
||||
},
|
||||
%{
|
||||
domain: "rejected.example.com",
|
||||
comment: "not enough #cofe posting",
|
||||
severity: "suspend",
|
||||
digest: "c15082d347fb7eccd251e199a98dcdce94a8dfc1d33d6ece80889eed374b6f92"
|
||||
},
|
||||
%{
|
||||
domain: "rejected-without-reason.example.com",
|
||||
severity: "suspend",
|
||||
digest: "48297155018c626c8867ca0bba4a2621eccdf51c6f7ac91b7099ed8b2ee9863f"
|
||||
}
|
||||
]
|
||||
|
||||
assert expected == output
|
||||
end
|
||||
end
|
||||
|
||||
describe "render translation_languages.json" do
|
||||
setup do: clear_config([Pleroma.Language.Translation, :provider], TranslationMock)
|
||||
|
||||
test "returns language matrix" do
|
||||
output = InstanceView.render("translation_languages.json", %{})
|
||||
|
||||
assert %{"en" => ["pl"], "pl" => ["en"]} == output
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -15,12 +15,28 @@ defmodule Pleroma.Web.MastodonAPI.ListViewTest do
|
|||
expected = %{
|
||||
id: to_string(list.id),
|
||||
title: title,
|
||||
exclusive: false
|
||||
exclusive: false,
|
||||
pleroma: %{
|
||||
emoji: nil,
|
||||
emoji_url: nil
|
||||
}
|
||||
}
|
||||
|
||||
assert expected == ListView.render("show.json", %{list: list})
|
||||
end
|
||||
|
||||
test "show with a unicode emoji" do
|
||||
user = insert(:user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "mortal enemies", emoji: "🕓"}, user)
|
||||
|
||||
assert %{
|
||||
pleroma: %{
|
||||
emoji: "🕓",
|
||||
emoji_url: nil
|
||||
}
|
||||
} = ListView.render("show.json", %{list: list})
|
||||
end
|
||||
|
||||
test "index" do
|
||||
user = insert(:user)
|
||||
|
||||
|
|
|
|||
|
|
@ -98,10 +98,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
|
|||
{:ok, favorite_activity} = CommonAPI.favorite(create_activity.id, another_user)
|
||||
{:ok, [notification]} = Notification.create_notifications(favorite_activity)
|
||||
create_activity = Activity.get_by_id(create_activity.id)
|
||||
assert is_binary(notification.group_key)
|
||||
|
||||
expected = %{
|
||||
id: to_string(notification.id),
|
||||
group_key: "ungrouped-#{to_string(notification.id)}",
|
||||
group_key: Notification.group_key(notification),
|
||||
pleroma: %{is_seen: false, is_muted: false},
|
||||
type: "favourite",
|
||||
account: AccountView.render("show.json", %{user: another_user, for: user}),
|
||||
|
|
@ -119,10 +120,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
|
|||
{:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, another_user)
|
||||
{:ok, [notification]} = Notification.create_notifications(reblog_activity)
|
||||
reblog_activity = Activity.get_by_id(create_activity.id)
|
||||
assert is_binary(notification.group_key)
|
||||
|
||||
expected = %{
|
||||
id: to_string(notification.id),
|
||||
group_key: "ungrouped-#{to_string(notification.id)}",
|
||||
group_key: Notification.group_key(notification),
|
||||
pleroma: %{is_seen: false, is_muted: false},
|
||||
type: "reblog",
|
||||
account: AccountView.render("show.json", %{user: another_user, for: user}),
|
||||
|
|
@ -138,10 +140,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
|
|||
followed = insert(:user)
|
||||
{:ok, followed, follower, _activity} = CommonAPI.follow(followed, follower)
|
||||
notification = Notification |> Repo.one() |> Repo.preload(:activity)
|
||||
assert is_binary(notification.group_key)
|
||||
|
||||
expected = %{
|
||||
id: to_string(notification.id),
|
||||
group_key: "ungrouped-#{to_string(notification.id)}",
|
||||
group_key: Notification.group_key(notification),
|
||||
pleroma: %{is_seen: false, is_muted: false},
|
||||
type: "follow",
|
||||
account: AccountView.render("show.json", %{user: follower, for: followed}),
|
||||
|
|
@ -333,7 +336,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
|
|||
|
||||
expected = %{
|
||||
id: to_string(notification.id),
|
||||
group_key: "ungrouped-#{to_string(notification.id)}",
|
||||
group_key: Notification.group_key(notification),
|
||||
pleroma: %{is_seen: true, is_muted: true},
|
||||
type: "favourite",
|
||||
account: AccountView.render("show.json", %{user: another_user, for: user}),
|
||||
|
|
|
|||
|
|
@ -668,6 +668,23 @@ 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
|
||||
|
|
@ -828,15 +845,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
|
||||
|
|
|
|||
|
|
@ -71,7 +71,10 @@ 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 -> %Conn{status: :success} end do
|
||||
call: fn _conn, _url, opts ->
|
||||
assert opts[:sniff_content_type]
|
||||
%Conn{status: :success}
|
||||
end do
|
||||
assert %Conn{status: :success} = get(conn, url)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@ defmodule Pleroma.Web.NodeInfoTest do
|
|||
default_features = [
|
||||
"pleroma_api",
|
||||
"mastodon_api",
|
||||
"mastodon_api_grouped_notifications",
|
||||
"mastodon_api_streaming",
|
||||
"polls",
|
||||
"pleroma_explicit_addressing",
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@
|
|||
defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
|
||||
use Pleroma.Web.ConnCase
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Tests.ObanHelpers
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
import Pleroma.Factory
|
||||
|
|
@ -105,6 +108,71 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
|
|||
assert length(response) == 1
|
||||
end
|
||||
|
||||
test "orders statuses by favorite date", %{
|
||||
conn: conn,
|
||||
user: user
|
||||
} do
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, first_post} = CommonAPI.post(other_user, %{status: "first post"})
|
||||
{:ok, second_post} = CommonAPI.post(other_user, %{status: "second post"})
|
||||
{:ok, third_post} = CommonAPI.post(other_user, %{status: "third post"})
|
||||
|
||||
{:ok, _} = CommonAPI.favorite(third_post.id, user)
|
||||
{:ok, _} = CommonAPI.favorite(first_post.id, user)
|
||||
{:ok, _} = CommonAPI.favorite(second_post.id, user)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|
||||
|> json_response_and_validate_schema(:ok)
|
||||
|
||||
assert Enum.map(response, & &1["id"]) == [second_post.id, first_post.id, third_post.id]
|
||||
end
|
||||
|
||||
test "returns a status once when duplicate favorite activities exist", %{
|
||||
conn: conn,
|
||||
user: user
|
||||
} do
|
||||
older_activity = insert(:note_activity)
|
||||
{:ok, _} = CommonAPI.favorite(older_activity.id, user)
|
||||
|
||||
activity = insert(:note_activity)
|
||||
{:ok, favorite} = CommonAPI.favorite(activity.id, user)
|
||||
|
||||
duplicate =
|
||||
Repo.insert!(%Activity{
|
||||
actor: favorite.actor,
|
||||
data: Map.put(favorite.data, "id", Utils.generate_activity_id()),
|
||||
local: favorite.local,
|
||||
recipients: favorite.recipients
|
||||
})
|
||||
|
||||
conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/favourites?limit=1")
|
||||
response = json_response_and_validate_schema(conn, :ok)
|
||||
|
||||
assert Enum.map(response, & &1["id"]) == [activity.id]
|
||||
assert [link_header] = get_resp_header(conn, "link")
|
||||
assert link_header =~ "max_id=#{duplicate.id}"
|
||||
assert link_header =~ "min_id=#{duplicate.id}"
|
||||
|
||||
response =
|
||||
conn
|
||||
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites?max_id=#{duplicate.id}&limit=1")
|
||||
|> json_response_and_validate_schema(:ok)
|
||||
|
||||
assert Enum.map(response, & &1["id"]) == [older_activity.id]
|
||||
|
||||
{:ok, _} = CommonAPI.unfavorite(activity.id, user)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> get("/api/v1/pleroma/accounts/#{user.id}/favourites")
|
||||
|> json_response_and_validate_schema(:ok)
|
||||
|
||||
assert Enum.map(response, & &1["id"]) == [older_activity.id]
|
||||
end
|
||||
|
||||
test "returns favorited DM only when user is logged in and he is one of recipients", %{
|
||||
current_user: current_user,
|
||||
user: user
|
||||
|
|
@ -164,23 +232,67 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
|
|||
} do
|
||||
activities = insert_list(10, :note_activity)
|
||||
|
||||
Enum.each(activities, fn activity ->
|
||||
CommonAPI.favorite(activity.id, user)
|
||||
end)
|
||||
favorite_pairs =
|
||||
Enum.map(activities, fn activity ->
|
||||
{:ok, favorite} = CommonAPI.favorite(activity.id, user)
|
||||
{favorite, activity}
|
||||
end)
|
||||
|> Enum.sort_by(fn {favorite, _activity} -> FlakeId.from_string(favorite.id) end)
|
||||
|
||||
third_activity = Enum.at(activities, 2)
|
||||
seventh_activity = Enum.at(activities, 6)
|
||||
{third_favorite, _} = Enum.at(favorite_pairs, 2)
|
||||
{seventh_favorite, _} = Enum.at(favorite_pairs, 6)
|
||||
|
||||
expected_ids =
|
||||
favorite_pairs
|
||||
|> Enum.slice(3, 3)
|
||||
|> Enum.reverse()
|
||||
|> Enum.map(fn {_favorite, activity} -> activity.id end)
|
||||
|
||||
response =
|
||||
conn
|
||||
|> get(
|
||||
"/api/v1/pleroma/accounts/#{user.id}/favourites?since_id=#{third_activity.id}&max_id=#{seventh_activity.id}"
|
||||
"/api/v1/pleroma/accounts/#{user.id}/favourites?since_id=#{third_favorite.id}&max_id=#{seventh_favorite.id}"
|
||||
)
|
||||
|> json_response_and_validate_schema(:ok)
|
||||
|
||||
assert length(response) == 3
|
||||
refute third_activity in response
|
||||
refute seventh_activity in response
|
||||
assert Enum.map(response, & &1["id"]) == expected_ids
|
||||
end
|
||||
|
||||
test "paginates favorites using min_id and limit", %{
|
||||
conn: conn,
|
||||
user: user
|
||||
} do
|
||||
activities = insert_list(10, :note_activity)
|
||||
|
||||
favorite_pairs =
|
||||
Enum.map(activities, fn activity ->
|
||||
{:ok, favorite} = CommonAPI.favorite(activity.id, user)
|
||||
{favorite, activity}
|
||||
end)
|
||||
|> Enum.sort_by(fn {favorite, _activity} -> FlakeId.from_string(favorite.id) end)
|
||||
|
||||
{third_favorite, _} = Enum.at(favorite_pairs, 2)
|
||||
{fourth_favorite, _} = Enum.at(favorite_pairs, 3)
|
||||
{fifth_favorite, _} = Enum.at(favorite_pairs, 4)
|
||||
|
||||
expected_ids =
|
||||
favorite_pairs
|
||||
|> Enum.slice(3, 2)
|
||||
|> Enum.reverse()
|
||||
|> Enum.map(fn {_favorite, activity} -> activity.id end)
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/v1/pleroma/accounts/#{user.id}/favourites?min_id=#{third_favorite.id}&limit=2"
|
||||
)
|
||||
|
||||
response = json_response_and_validate_schema(conn, :ok)
|
||||
|
||||
assert Enum.map(response, & &1["id"]) == expected_ids
|
||||
assert [link_header] = get_resp_header(conn, "link")
|
||||
assert link_header =~ "max_id=#{fourth_favorite.id}"
|
||||
assert link_header =~ "min_id=#{fifth_favorite.id}"
|
||||
end
|
||||
|
||||
test "limits favorites using limit parameter", %{
|
||||
|
|
|
|||
|
|
@ -546,6 +546,14 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do
|
|||
account_ids = Enum.map(result, &get_in(&1, ["account", "id"]))
|
||||
assert Enum.sort(account_ids) == Enum.sort([user2.id, user3.id])
|
||||
end
|
||||
|
||||
test "it returns 404 when chats are disabled", %{conn: conn} do
|
||||
clear_config([Pleroma.Chat, :enabled], false)
|
||||
|
||||
conn
|
||||
|> get(unquote(tested_endpoint))
|
||||
|> json_response_and_validate_schema(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -122,16 +122,27 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do
|
|||
|
||||
clear_config([:frontends, :pickable], ["#{name}/#{ref}"])
|
||||
path = "#{@dir}/frontends/#{name}/#{ref}"
|
||||
path2 = "#{@dir}/frontends/#{name}/#{ref}/packs/html"
|
||||
|
||||
Pleroma.Backports.mkdir_p!(path)
|
||||
File.write!("#{path}/index.html", "from frontend plug")
|
||||
|
||||
Pleroma.Backports.mkdir_p!(path2)
|
||||
File.write!("#{path2}/custom.html", "from frontend plug")
|
||||
|
||||
index =
|
||||
conn
|
||||
|> put_req_cookie("preferred_frontend", "#{name}/#{ref}")
|
||||
|> get("/")
|
||||
|
||||
assert html_response(index, 200) == "from frontend plug"
|
||||
|
||||
custom =
|
||||
conn
|
||||
|> put_req_cookie("preferred_frontend", "#{name}/#{ref}")
|
||||
|> get("/packs/html/custom.html")
|
||||
|
||||
assert html_response(custom, 200) == "from frontend plug"
|
||||
end
|
||||
|
||||
test "only returns content from pickable frontends", %{conn: conn} do
|
||||
|
|
|
|||
|
|
@ -61,6 +61,28 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do
|
|||
File.write!(@dir <> "/static/kaniini.html", "<h1>rabbit hugs as a service</h1>")
|
||||
index = get(build_conn(), "/static/kaniini.html")
|
||||
assert html_response(index, 200) == "<h1>rabbit hugs as a service</h1>"
|
||||
|
||||
File.write!(@dir <> "/static/kaniini the bnuuy.html", "<h1>rabbit hugs as a service</h1>")
|
||||
index = get(build_conn(), "/static/kaniini%20the%20bnuuy.html")
|
||||
assert html_response(index, 200) == "<h1>rabbit hugs as a service</h1>"
|
||||
end
|
||||
|
||||
test "overrides /static/styles files with spaces in URL" do
|
||||
Pleroma.Backports.mkdir_p!(@dir <> "/static/styles")
|
||||
File.write(@dir <> "/static/styles/Redmond DX.iss", "overridden")
|
||||
|
||||
index = get(build_conn(), "/static/styles/Redmond%20DX.iss")
|
||||
assert index.status == 200
|
||||
assert index.resp_body == File.read!(@dir <> "/static/styles/Redmond DX.iss")
|
||||
assert index.halted == true
|
||||
end
|
||||
|
||||
test "serves fallback /static/styles files with spaces in URL" do
|
||||
index = get(build_conn(), "/static/styles/Redmond%20DX.iss")
|
||||
|
||||
assert index.status == 200
|
||||
assert index.resp_body == File.read!("priv/static/static/styles/Redmond DX.iss")
|
||||
assert index.halted == true
|
||||
end
|
||||
|
||||
test "does not sanitize dangerous files in general, as there can be html and javascript files legitimately in this folder" do
|
||||
|
|
@ -137,47 +159,4 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do
|
|||
# It should be preserved because "image" is in the allowed_mime_types list
|
||||
assert content_type == "image/jpeg"
|
||||
end
|
||||
|
||||
describe "404s for missing files in static-only paths" do
|
||||
test "returns 404 for non-existent static-only JSON files" do
|
||||
conn = get(build_conn(), "/static/non-existent.json")
|
||||
|
||||
assert conn.status == 404
|
||||
assert ["application/json"] = get_resp_header(conn, "content-type")
|
||||
assert Jason.decode!(conn.resp_body) == %{"error" => "not found"}
|
||||
end
|
||||
|
||||
test "returns 404 for non-existent static-only non-JSON files" do
|
||||
conn = get(build_conn(), "/static/non-existent.txt")
|
||||
|
||||
assert conn.status == 404
|
||||
assert conn.resp_body == "Not found"
|
||||
assert ["text/plain"] = get_resp_header(conn, "content-type")
|
||||
end
|
||||
|
||||
test "returns 404 for non-existent .css files" do
|
||||
conn = get(build_conn(), "/static/non-existent.css")
|
||||
|
||||
assert conn.status == 404
|
||||
assert conn.resp_body == "Not found"
|
||||
# Verifies that we forced text/plain for the error body, even though the path was .css
|
||||
assert ["text/plain"] = get_resp_header(conn, "content-type")
|
||||
end
|
||||
|
||||
test "returns 404 for non-existent files without an extension" do
|
||||
conn = get(build_conn(), "/static/non-existent")
|
||||
|
||||
assert conn.status == 404
|
||||
assert conn.resp_body == "Not found"
|
||||
assert ["text/plain"] = get_resp_header(conn, "content-type")
|
||||
end
|
||||
|
||||
test "returns 200 (falls through to SPA) for non-static-only paths" do
|
||||
# /some-route is NOT in static_only_files, so it should still fall through to the SPA.
|
||||
conn = get(build_conn(), "/some-route")
|
||||
|
||||
assert conn.status == 200
|
||||
assert ["text/html; charset=utf-8"] = get_resp_header(conn, "content-type")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -44,6 +44,18 @@ defmodule Pleroma.Web.Plugs.OAuthPlugTest do
|
|||
assert conn.assigns[:user] == opts[:user]
|
||||
end
|
||||
|
||||
test "with a valid token, it does not preload the duplicate user association", %{
|
||||
conn: conn,
|
||||
token: token
|
||||
} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("authorization", "bearer #{token.token}")
|
||||
|> OAuthPlug.call(%{})
|
||||
|
||||
assert %Ecto.Association.NotLoaded{} = conn.assigns.token.user
|
||||
end
|
||||
|
||||
test "with valid token (downcase) in url parameters, it assigns the user", opts do
|
||||
conn =
|
||||
:get
|
||||
|
|
|
|||
49
test/pleroma/web/plugs/static_not_found_plug_test.exs
Normal file
49
test/pleroma/web/plugs/static_not_found_plug_test.exs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
defmodule Pleroma.Web.Plugs.StaticNotFoundPlugTest do
|
||||
use Pleroma.Web.ConnCase
|
||||
|
||||
describe "404s for missing files in static-only paths" do
|
||||
test "returns 404 for non-existent static-only JSON files" do
|
||||
conn = get(build_conn(), "/static/non-existent.json")
|
||||
|
||||
assert conn.status == 404
|
||||
assert ["application/json"] = get_resp_header(conn, "content-type")
|
||||
assert Jason.decode!(conn.resp_body) == %{"error" => "not found"}
|
||||
end
|
||||
|
||||
test "returns 404 for non-existent static-only non-JSON files" do
|
||||
conn = get(build_conn(), "/static/non-existent.txt")
|
||||
|
||||
assert conn.status == 404
|
||||
assert conn.resp_body == "Not found"
|
||||
assert ["text/plain"] = get_resp_header(conn, "content-type")
|
||||
end
|
||||
|
||||
test "returns 404 for non-existent .css files" do
|
||||
conn = get(build_conn(), "/static/non-existent.css")
|
||||
|
||||
assert conn.status == 404
|
||||
assert conn.resp_body == "Not found"
|
||||
# Verifies that we forced text/plain for the error body, even though the path was .css
|
||||
assert ["text/plain"] = get_resp_header(conn, "content-type")
|
||||
end
|
||||
|
||||
test "returns 404 for non-existent files without an extension" do
|
||||
conn = get(build_conn(), "/static/non-existent")
|
||||
|
||||
assert conn.status == 404
|
||||
assert conn.resp_body == "Not found"
|
||||
assert ["text/plain"] = get_resp_header(conn, "content-type")
|
||||
end
|
||||
|
||||
test "returns 200 (falls through to SPA) for non-static-only paths" do
|
||||
# /some-route is NOT in static_only_files, so it should still fall through to the SPA.
|
||||
conn = get(build_conn(), "/some-route")
|
||||
|
||||
assert conn.status == 200
|
||||
assert ["text/html; charset=utf-8"] = get_resp_header(conn, "content-type")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -475,6 +475,20 @@ defmodule Pleroma.Web.StreamerTest do
|
|||
assert Streamer.filtered_by_user?(user, favorite_activity)
|
||||
end
|
||||
|
||||
test "it does not filter followed users on blocked domains", %{user: user} do
|
||||
followed =
|
||||
insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/friend"})
|
||||
|
||||
{:ok, user} = User.block_domain(user, "hecking-lewd-place.com")
|
||||
{:ok, activity} = CommonAPI.post(followed, %{status: "still a friend"})
|
||||
|
||||
assert Streamer.filtered_by_user?(user, activity)
|
||||
|
||||
{:ok, user, _followed} = User.follow(user, followed)
|
||||
|
||||
refute Streamer.filtered_by_user?(user, activity)
|
||||
end
|
||||
|
||||
test "it sends follow activities to the 'user:notification' stream", %{
|
||||
user: user,
|
||||
token: oauth_token
|
||||
|
|
|
|||
|
|
@ -72,4 +72,54 @@ defmodule Pleroma.Webhook.NotifyTest do
|
|||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -33,6 +33,15 @@ defmodule Pleroma.WebhookTest do
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ defmodule Pleroma.Workers.PublisherWorkerTest do
|
|||
use Pleroma.DataCase, async: false
|
||||
use Oban.Testing, repo: Pleroma.Repo
|
||||
|
||||
import Mox
|
||||
import Pleroma.Factory
|
||||
import Mock
|
||||
|
||||
alias Pleroma.Instances
|
||||
alias Pleroma.Object
|
||||
|
|
@ -15,6 +15,8 @@ defmodule Pleroma.Workers.PublisherWorkerTest do
|
|||
alias Pleroma.Web.ActivityPub.Builder
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.Federator
|
||||
alias Pleroma.Web.FederatorMock
|
||||
alias Pleroma.Workers.PublisherWorker
|
||||
|
||||
describe "Oban job priority:" do
|
||||
setup do
|
||||
|
|
@ -55,49 +57,80 @@ defmodule Pleroma.Workers.PublisherWorkerTest do
|
|||
end
|
||||
|
||||
test "marks server as unreachable only on final failure", %{activity: activity} do
|
||||
with_mock Pleroma.Web.Federator,
|
||||
perform: fn :publish_one, _params -> {:error, :connection_error} end do
|
||||
# First attempt
|
||||
job = %Oban.Job{
|
||||
args: %{
|
||||
"op" => "publish_one",
|
||||
"params" => %{
|
||||
"inbox" => "https://example.com/inbox",
|
||||
"activity_id" => activity.id
|
||||
}
|
||||
},
|
||||
attempt: 1,
|
||||
max_attempts: 5
|
||||
}
|
||||
expect(FederatorMock, :perform, 2, fn :publish_one, _params ->
|
||||
{:error, :connection_error}
|
||||
end)
|
||||
|
||||
assert {:error, :connection_error} = Pleroma.Workers.PublisherWorker.perform(job)
|
||||
assert Instances.reachable?("https://example.com/inbox")
|
||||
# First attempt
|
||||
job = %Oban.Job{
|
||||
args: %{
|
||||
"op" => "publish_one",
|
||||
"params" => %{
|
||||
"inbox" => "https://example.com/inbox",
|
||||
"activity_id" => activity.id
|
||||
}
|
||||
},
|
||||
attempt: 1,
|
||||
max_attempts: 5
|
||||
}
|
||||
|
||||
# Final attempt
|
||||
job = %{job | attempt: 5}
|
||||
assert {:error, :connection_error} = Pleroma.Workers.PublisherWorker.perform(job)
|
||||
refute Instances.reachable?("https://example.com/inbox")
|
||||
end
|
||||
assert {:error, :connection_error} = PublisherWorker.perform(job)
|
||||
assert Instances.reachable?("https://example.com/inbox")
|
||||
|
||||
# Final attempt
|
||||
job = %{job | attempt: 5}
|
||||
assert {:error, :connection_error} = PublisherWorker.perform(job)
|
||||
refute Instances.reachable?("https://example.com/inbox")
|
||||
end
|
||||
|
||||
test "does not mark server as unreachable on successful publish", %{activity: activity} do
|
||||
with_mock Pleroma.Web.Federator,
|
||||
perform: fn :publish_one, _params -> {:ok, %{status: 200}} end do
|
||||
job = %Oban.Job{
|
||||
args: %{
|
||||
"op" => "publish_one",
|
||||
"params" => %{
|
||||
"inbox" => "https://example.com/inbox",
|
||||
"activity_id" => activity.id
|
||||
}
|
||||
},
|
||||
attempt: 1,
|
||||
max_attempts: 5
|
||||
}
|
||||
expect(FederatorMock, :perform, fn :publish_one, _params -> {:ok, %{status: 200}} end)
|
||||
|
||||
assert :ok = Pleroma.Workers.PublisherWorker.perform(job)
|
||||
assert Instances.reachable?("https://example.com/inbox")
|
||||
end
|
||||
job = %Oban.Job{
|
||||
args: %{
|
||||
"op" => "publish_one",
|
||||
"params" => %{
|
||||
"inbox" => "https://example.com/inbox",
|
||||
"activity_id" => activity.id
|
||||
}
|
||||
},
|
||||
attempt: 1,
|
||||
max_attempts: 5
|
||||
}
|
||||
|
||||
assert :ok = PublisherWorker.perform(job)
|
||||
assert Instances.reachable?("https://example.com/inbox")
|
||||
end
|
||||
|
||||
test "does not create atoms from unexpected job params", %{activity: activity} do
|
||||
unknown_key = "unknown_#{System.unique_integer([:positive])}"
|
||||
|
||||
assert_raise ArgumentError, fn -> String.to_existing_atom(unknown_key) end
|
||||
|
||||
expect(FederatorMock, :perform, fn :publish_one, params ->
|
||||
assert params == %{
|
||||
inbox: "https://example.com/inbox",
|
||||
activity_id: activity.id
|
||||
}
|
||||
|
||||
{:ok, %{status: 200}}
|
||||
end)
|
||||
|
||||
job = %Oban.Job{
|
||||
args: %{
|
||||
"op" => "publish_one",
|
||||
"params" => %{
|
||||
"inbox" => "https://example.com/inbox",
|
||||
"activity_id" => activity.id,
|
||||
unknown_key => "ignored"
|
||||
}
|
||||
},
|
||||
attempt: 1,
|
||||
max_attempts: 5
|
||||
}
|
||||
|
||||
assert :ok = PublisherWorker.perform(job)
|
||||
assert_raise ArgumentError, fn -> String.to_existing_atom(unknown_key) end
|
||||
end
|
||||
|
||||
test "cancels job if server is unreachable", %{activity: activity} do
|
||||
|
|
@ -117,7 +150,7 @@ defmodule Pleroma.Workers.PublisherWorkerTest do
|
|||
max_attempts: 5
|
||||
}
|
||||
|
||||
assert {:cancel, :unreachable} = Pleroma.Workers.PublisherWorker.perform(job)
|
||||
assert {:cancel, :unreachable} = PublisherWorker.perform(job)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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, [])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue