Merge branch 'develop' into audit-mix-audit-report
This commit is contained in:
commit
45459c8398
156 changed files with 8723 additions and 731 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
|
||||
|
|
|
|||
|
|
@ -417,7 +417,8 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
|
|||
["user_invite_tokens"],
|
||||
["user_notes"],
|
||||
["user_relationships"],
|
||||
["users"]
|
||||
["users"],
|
||||
["webhooks"]
|
||||
]
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
174
test/pleroma/repo/optional_migrations/rebased_rollbacks_test.exs
Normal file
174
test/pleroma/repo/optional_migrations/rebased_rollbacks_test.exs
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Repo.OptionalMigrations.RebasedRollbacksTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Ecto.Adapters.SQL
|
||||
|
||||
defmodule MigrationRepo do
|
||||
use Ecto.Repo,
|
||||
otp_app: :pleroma,
|
||||
adapter: Ecto.Adapters.Postgres
|
||||
end
|
||||
|
||||
@migrations_path "priv/repo/migrations"
|
||||
@rollbacks_path "priv/repo/optional_migrations/rebased_rollbacks"
|
||||
|
||||
@expected_versions ~w{
|
||||
20210612185407
|
||||
20220314220000
|
||||
20220819171321
|
||||
20220927220033
|
||||
20221207235104
|
||||
}
|
||||
|
||||
test "only includes Rebased-specific migration versions" do
|
||||
rollback_versions = migration_versions(@rollbacks_path)
|
||||
pleroma_versions = migration_versions(@migrations_path)
|
||||
|
||||
assert rollback_versions == @expected_versions
|
||||
assert MapSet.disjoint?(MapSet.new(rollback_versions), MapSet.new(pleroma_versions))
|
||||
end
|
||||
|
||||
test "rolls back Rebased schema without removing Pleroma data" do
|
||||
prefix = "rebased_rollbacks_#{Ecto.UUID.generate() |> String.replace("-", "")}"
|
||||
|
||||
repo_config = [
|
||||
username: "postgres",
|
||||
password: "postgres",
|
||||
database: System.get_env("DB_NAME", "pleroma_test"),
|
||||
hostname: System.get_env("DB_HOST", "localhost"),
|
||||
port: String.to_integer(System.get_env("DB_PORT", "5432")),
|
||||
pool_size: 2,
|
||||
parameters: [search_path: prefix]
|
||||
]
|
||||
|
||||
{:ok, repo} = MigrationRepo.start_link(repo_config)
|
||||
Process.unlink(repo)
|
||||
|
||||
on_exit(fn ->
|
||||
query!("DROP SCHEMA IF EXISTS #{prefix} CASCADE")
|
||||
MigrationRepo.stop()
|
||||
end)
|
||||
|
||||
query!("CREATE SCHEMA #{prefix}")
|
||||
|
||||
create_rebased_schema()
|
||||
|
||||
expected_rollbacks =
|
||||
@expected_versions
|
||||
|> Enum.reverse()
|
||||
|> Enum.map(&String.to_integer/1)
|
||||
|
||||
assert Ecto.Migrator.run(MigrationRepo, @rollbacks_path, :down, all: true) ==
|
||||
expected_rollbacks
|
||||
|
||||
assert query!("SELECT version FROM schema_migrations ORDER BY version").rows == [
|
||||
[20_220_624_104_914],
|
||||
[20_221_029_171_353]
|
||||
]
|
||||
|
||||
assert query!("""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema() AND table_name = 'users'
|
||||
ORDER BY column_name
|
||||
""").rows == [["id"]]
|
||||
|
||||
assert query!("SELECT type::text FROM notifications ORDER BY type::text").rows == [["status"]]
|
||||
|
||||
assert query!("""
|
||||
SELECT enumlabel
|
||||
FROM pg_enum
|
||||
JOIN pg_type ON pg_type.oid = enumtypid
|
||||
JOIN pg_namespace ON pg_namespace.oid = pg_type.typnamespace
|
||||
WHERE typname = 'notification_type' AND nspname = current_schema()
|
||||
ORDER BY enumsortorder
|
||||
""").rows ==
|
||||
Enum.map(
|
||||
~w{follow follow_request mention move pleroma:emoji_reaction pleroma:chat_mention reblog favourite pleroma:report poll status update},
|
||||
&[&1]
|
||||
)
|
||||
|
||||
assert query!("SELECT to_regclass('objects_events')").rows == [[nil]]
|
||||
|
||||
assert query!("SELECT url, secret, internal FROM webhooks").rows == [
|
||||
["https://example.com/webhook", "preserve-me", false]
|
||||
]
|
||||
end
|
||||
|
||||
defp migration_versions(path) do
|
||||
path
|
||||
|> Path.join("*.exs")
|
||||
|> Path.wildcard()
|
||||
|> Enum.map(fn migration ->
|
||||
migration
|
||||
|> Path.basename()
|
||||
|> String.split("_", parts: 2)
|
||||
|> hd()
|
||||
end)
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
defp create_rebased_schema do
|
||||
statements = [
|
||||
"""
|
||||
CREATE TYPE notification_type AS ENUM (
|
||||
'follow', 'follow_request', 'mention', 'move', 'pleroma:emoji_reaction',
|
||||
'pleroma:chat_mention', 'reblog', 'favourite', 'pleroma:report', 'poll',
|
||||
'status', 'update', 'pleroma:participation_accepted',
|
||||
'pleroma:participation_request', 'pleroma:event_reminder',
|
||||
'pleroma:event_update'
|
||||
)
|
||||
""",
|
||||
"CREATE TABLE notifications (id bigserial PRIMARY KEY, type notification_type)",
|
||||
"""
|
||||
INSERT INTO notifications (type) VALUES
|
||||
('status'),
|
||||
('pleroma:participation_accepted'),
|
||||
('pleroma:participation_request'),
|
||||
('pleroma:event_reminder'),
|
||||
('pleroma:event_update')
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE users (
|
||||
id bigserial PRIMARY KEY,
|
||||
accepts_email_list boolean,
|
||||
location varchar(255),
|
||||
last_move_at timestamp
|
||||
)
|
||||
""",
|
||||
"CREATE TABLE objects (id bigserial PRIMARY KEY, data jsonb)",
|
||||
"CREATE INDEX objects_events ON objects ((data->>'type')) WHERE data->>'type' = 'Event'",
|
||||
"""
|
||||
CREATE TABLE webhooks (
|
||||
id bigserial PRIMARY KEY,
|
||||
url varchar(255) NOT NULL,
|
||||
events varchar[] NOT NULL DEFAULT '{}',
|
||||
secret varchar(255) NOT NULL DEFAULT '',
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
internal boolean NOT NULL DEFAULT false
|
||||
)
|
||||
""",
|
||||
"CREATE UNIQUE INDEX webhooks_url_index ON webhooks (url)",
|
||||
"INSERT INTO webhooks (url, secret) VALUES ('https://example.com/webhook', 'preserve-me')",
|
||||
"CREATE TABLE schema_migrations (version bigint PRIMARY KEY, inserted_at timestamp(0))",
|
||||
"""
|
||||
INSERT INTO schema_migrations (version, inserted_at) VALUES
|
||||
(20210612185407, NOW()),
|
||||
(20220314220000, NOW()),
|
||||
(20220624104914, NOW()),
|
||||
(20220819171321, NOW()),
|
||||
(20220927220033, NOW()),
|
||||
(20221029171353, NOW()),
|
||||
(20221207235104, NOW())
|
||||
"""
|
||||
]
|
||||
|
||||
Enum.each(statements, &query!/1)
|
||||
end
|
||||
|
||||
defp query!(statement), do: SQL.query!(MigrationRepo, statement, [])
|
||||
end
|
||||
|
|
@ -11,6 +11,15 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
alias Pleroma.ReverseProxy.ClientMock
|
||||
alias Plug.Conn
|
||||
|
||||
defmodule ProxyPlug do
|
||||
@moduledoc false
|
||||
|
||||
def init(options), do: options
|
||||
|
||||
def call(conn, {url, opts}), do: Pleroma.ReverseProxy.call(conn, url, opts)
|
||||
def call(conn, url), do: Pleroma.ReverseProxy.call(conn, url)
|
||||
end
|
||||
|
||||
setup_all do
|
||||
{:ok, _} = Registry.start_link(keys: :unique, name: ClientMock)
|
||||
:ok
|
||||
|
|
@ -33,6 +42,88 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
:ok
|
||||
end
|
||||
|
||||
defp start_bandit(url, opts \\ []) do
|
||||
start_supervised!(
|
||||
{Bandit,
|
||||
plug: {ProxyPlug, {url, opts}},
|
||||
ip: {127, 0, 0, 1},
|
||||
port: 0,
|
||||
http_options: [log_exceptions_with_status_codes: []]}
|
||||
)
|
||||
end
|
||||
|
||||
defp start_cowboy(url, opts \\ []) do
|
||||
ref = {__MODULE__, make_ref()}
|
||||
|
||||
{:ok, _pid} =
|
||||
Plug.Cowboy.http(ProxyPlug, {url, opts},
|
||||
ip: {127, 0, 0, 1},
|
||||
port: 0,
|
||||
ref: ref,
|
||||
protocol_options: [stream_handlers: [:cowboy_stream_h]]
|
||||
)
|
||||
|
||||
on_exit(fn -> Plug.Cowboy.shutdown(ref) end)
|
||||
:ranch.get_port(ref)
|
||||
end
|
||||
|
||||
defp bandit_request(url, headers \\ [], opts \\ []) do
|
||||
pid = start_bandit(url, opts)
|
||||
{:ok, {{127, 0, 0, 1}, port}} = ThousandIsland.listener_info(pid)
|
||||
http1_request(port, headers)
|
||||
end
|
||||
|
||||
defp cowboy_request(url, headers \\ [], opts \\ []) do
|
||||
url
|
||||
|> start_cowboy(opts)
|
||||
|> http1_request(headers)
|
||||
end
|
||||
|
||||
defp http1_request(port, headers) do
|
||||
{:ok, socket} = :gen_tcp.connect({127, 0, 0, 1}, port, [:binary, active: false])
|
||||
|
||||
request_headers =
|
||||
[["host: localhost", "connection: close"] | headers]
|
||||
|> List.flatten()
|
||||
|> Enum.join("\r\n")
|
||||
|
||||
:ok = :gen_tcp.send(socket, "GET / HTTP/1.1\r\n#{request_headers}\r\n\r\n")
|
||||
recv_all(socket, [])
|
||||
end
|
||||
|
||||
defp recv_all(socket, chunks) do
|
||||
case :gen_tcp.recv(socket, 0, 5_000) do
|
||||
{:ok, data} -> recv_all(socket, [data | chunks])
|
||||
{:error, reason} -> {reason, chunks |> Enum.reverse() |> IO.iodata_to_binary()}
|
||||
end
|
||||
end
|
||||
|
||||
defp receive_mint_responses(conn, responses \\ []) do
|
||||
receive do
|
||||
message ->
|
||||
case Mint.HTTP2.stream(conn, message) do
|
||||
{:ok, conn, new_responses} ->
|
||||
responses = responses ++ new_responses
|
||||
|
||||
if Enum.any?(new_responses, fn response ->
|
||||
elem(response, 0) in [:done, :error]
|
||||
end) do
|
||||
responses
|
||||
else
|
||||
receive_mint_responses(conn, responses)
|
||||
end
|
||||
|
||||
{:error, _conn, error, new_responses} ->
|
||||
responses ++ new_responses ++ [{:connection_error, error}]
|
||||
|
||||
:unknown ->
|
||||
receive_mint_responses(conn, responses)
|
||||
end
|
||||
after
|
||||
2_000 -> responses ++ [:timeout]
|
||||
end
|
||||
end
|
||||
|
||||
defp request_mock(invokes) do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, url, headers, _body, _opts ->
|
||||
|
|
@ -58,6 +149,45 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
end)
|
||||
end
|
||||
|
||||
defp stream_then_error_mock(url, headers, chunk \\ "partial", error \\ :closed) do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, ^url, _, _, _ ->
|
||||
Registry.register(ClientMock, url, 0)
|
||||
{:ok, 200, headers, %{url: url}}
|
||||
end)
|
||||
|> expect(:stream_body, 2, fn %{url: ^url} = client ->
|
||||
case Registry.lookup(ClientMock, url) do
|
||||
[{_, 0}] ->
|
||||
Registry.update_value(ClientMock, url, &(&1 + 1))
|
||||
{:ok, chunk, client}
|
||||
|
||||
[{_, 1}] ->
|
||||
Registry.unregister(ClientMock, url)
|
||||
{:error, error}
|
||||
end
|
||||
end)
|
||||
|> expect(:close, fn _ -> :ok end)
|
||||
end
|
||||
|
||||
defp complete_stream_mock(url, headers, body) do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, ^url, _, _, _ ->
|
||||
Registry.register(ClientMock, url, 0)
|
||||
{:ok, 200, headers, %{url: url}}
|
||||
end)
|
||||
|> expect(:stream_body, 2, fn %{url: ^url} = client ->
|
||||
case Registry.lookup(ClientMock, url) do
|
||||
[{_, 0}] ->
|
||||
Registry.update_value(ClientMock, url, &(&1 + 1))
|
||||
{:ok, body, client}
|
||||
|
||||
[{_, 1}] ->
|
||||
Registry.unregister(ClientMock, url)
|
||||
:done
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
describe "reverse proxy" do
|
||||
test "do not track successful request", %{conn: conn} do
|
||||
request_mock(2)
|
||||
|
|
@ -179,14 +309,202 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
assert response == %{"user-agent" => Pleroma.Application.user_agent()}
|
||||
end
|
||||
|
||||
test "closed connection", %{conn: conn} do
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/closed", _, _, _ -> {:ok, 200, [], %{}} end)
|
||||
|> expect(:stream_body, fn _ -> {:error, :closed} end)
|
||||
|> expect(:close, fn _ -> :ok end)
|
||||
test "aborts a partially streamed response when the upstream closes", %{conn: conn} do
|
||||
stream_then_error_mock("/closed", [{"content-length", "20"}])
|
||||
|
||||
conn = ReverseProxy.call(conn, "/closed")
|
||||
assert conn.halted
|
||||
assert_raise ReverseProxy.StreamError, fn ->
|
||||
ReverseProxy.call(conn, "/closed")
|
||||
end
|
||||
|
||||
assert Cachex.get(:failed_proxy_url_cache, "/closed") == {:ok, true}
|
||||
end
|
||||
|
||||
describe "Bandit HTTP/1 streaming" do
|
||||
test "closes an incomplete chunked response without a terminating chunk" do
|
||||
Mox.set_mox_global(ClientMock)
|
||||
stream_then_error_mock("/bandit-closed", [{"content-type", "image/png"}])
|
||||
|
||||
assert {:closed, response} = bandit_request("/bandit-closed")
|
||||
assert response =~ "transfer-encoding: chunked"
|
||||
assert response =~ "partial"
|
||||
refute response =~ "0\r\n\r\n"
|
||||
end
|
||||
|
||||
test "closes an incomplete fixed-length response before the declared length" do
|
||||
Mox.set_mox_global(ClientMock)
|
||||
|
||||
stream_then_error_mock("/bandit-fixed-closed", [
|
||||
{"content-length", "20"},
|
||||
{"content-type", "image/png"}
|
||||
])
|
||||
|
||||
assert {:closed, response} = bandit_request("/bandit-fixed-closed")
|
||||
assert response =~ "content-length: 20"
|
||||
refute response =~ "transfer-encoding"
|
||||
assert String.ends_with?(response, "partial")
|
||||
end
|
||||
|
||||
test "streams a complete fixed-length response without transformation" do
|
||||
Mox.set_mox_global(ClientMock)
|
||||
body = "complete body"
|
||||
|
||||
complete_stream_mock(
|
||||
"/bandit-complete",
|
||||
[
|
||||
{"content-length", to_string(byte_size(body))},
|
||||
{"content-type", "image/png"}
|
||||
],
|
||||
body
|
||||
)
|
||||
|
||||
assert {:closed, response} =
|
||||
bandit_request("/bandit-complete", ["accept-encoding: deflate"])
|
||||
|
||||
assert response =~ "content-length: #{byte_size(body)}"
|
||||
assert response =~ "cache-control: public, max-age=1209600, immutable, no-transform"
|
||||
refute response =~ "content-encoding: gzip"
|
||||
refute response =~ "content-encoding: deflate"
|
||||
assert String.ends_with?(response, body)
|
||||
end
|
||||
|
||||
test "keeps no-transform when custom response headers replace cache-control" do
|
||||
Mox.set_mox_global(ClientMock)
|
||||
body = "complete body"
|
||||
|
||||
complete_stream_mock(
|
||||
"/bandit-custom-cache-control",
|
||||
[
|
||||
{"content-length", to_string(byte_size(body))},
|
||||
{"content-type", "image/png"}
|
||||
],
|
||||
body
|
||||
)
|
||||
|
||||
assert {:closed, response} =
|
||||
bandit_request(
|
||||
"/bandit-custom-cache-control",
|
||||
["accept-encoding: deflate"],
|
||||
resp_headers: [{"cache-control", "private, max-age=60"}]
|
||||
)
|
||||
|
||||
assert response =~ "content-length: #{byte_size(body)}"
|
||||
assert response =~ "cache-control: private, max-age=60, no-transform"
|
||||
refute response =~ "content-encoding: gzip"
|
||||
refute response =~ "content-encoding: deflate"
|
||||
assert String.ends_with?(response, body)
|
||||
end
|
||||
|
||||
test "does not blacklist the upstream when the downstream disconnects" do
|
||||
Mox.set_mox_global(ClientMock)
|
||||
test_pid = self()
|
||||
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/bandit-client-closed", _, _, _ ->
|
||||
{:ok, 200, [{"content-type", "image/png"}], %{url: "/bandit-client-closed"}}
|
||||
end)
|
||||
|> stub(:stream_body, fn client ->
|
||||
send(test_pid, {:stream_requested, self()})
|
||||
|
||||
receive do
|
||||
:continue -> {:ok, :binary.copy("x", 1_000_000), client}
|
||||
after
|
||||
1_000 -> raise "timed out waiting for downstream disconnect"
|
||||
end
|
||||
end)
|
||||
|> expect(:close, fn _ ->
|
||||
send(test_pid, :upstream_closed)
|
||||
:ok
|
||||
end)
|
||||
|
||||
pid = start_bandit("/bandit-client-closed")
|
||||
{:ok, {{127, 0, 0, 1}, port}} = ThousandIsland.listener_info(pid)
|
||||
{:ok, socket} = :gen_tcp.connect({127, 0, 0, 1}, port, [:binary, active: false])
|
||||
:ok = :gen_tcp.send(socket, "GET / HTTP/1.1\r\nhost: localhost\r\n\r\n")
|
||||
|
||||
assert_receive {:stream_requested, request_pid}
|
||||
:ok = :inet.setopts(socket, linger: {true, 0})
|
||||
:ok = :gen_tcp.close(socket)
|
||||
send(request_pid, :continue)
|
||||
|
||||
assert_receive :upstream_closed
|
||||
assert Cachex.get(:failed_proxy_url_cache, "/bandit-client-closed") == {:ok, nil}
|
||||
end
|
||||
end
|
||||
|
||||
describe "Cowboy streaming" do
|
||||
test "closes an incomplete HTTP/1 chunked response without a terminating chunk" do
|
||||
Mox.set_mox_global(ClientMock)
|
||||
stream_then_error_mock("/cowboy-closed", [{"content-type", "image/png"}])
|
||||
|
||||
assert {:closed, response} = cowboy_request("/cowboy-closed")
|
||||
assert response =~ "transfer-encoding: chunked"
|
||||
assert response =~ "partial"
|
||||
refute response =~ "0\r\n\r\n"
|
||||
end
|
||||
|
||||
test "closes an incomplete HTTP/1 fixed-length response before the declared length" do
|
||||
Mox.set_mox_global(ClientMock)
|
||||
|
||||
stream_then_error_mock("/cowboy-fixed-closed", [
|
||||
{"content-length", "20"},
|
||||
{"content-type", "image/png"}
|
||||
])
|
||||
|
||||
assert {:closed, response} = cowboy_request("/cowboy-fixed-closed")
|
||||
assert response =~ "content-length: 20"
|
||||
refute response =~ "transfer-encoding"
|
||||
assert String.ends_with?(response, "partial")
|
||||
end
|
||||
|
||||
test "resets an incomplete HTTP/2 stream" do
|
||||
Mox.set_mox_global(ClientMock)
|
||||
stream_then_error_mock("/cowboy-http2-closed", [{"content-type", "image/png"}])
|
||||
|
||||
port = start_cowboy("/cowboy-http2-closed")
|
||||
{:ok, mint} = Mint.HTTP2.connect(:http, "localhost", port)
|
||||
{:ok, mint, request_ref} = Mint.HTTP2.request(mint, "GET", "/", [], nil)
|
||||
responses = receive_mint_responses(mint)
|
||||
|
||||
assert {:status, request_ref, 200} in responses
|
||||
assert {:data, request_ref, "partial"} in responses
|
||||
|
||||
assert Enum.any?(responses, fn
|
||||
{:error, ^request_ref, %Mint.HTTPError{reason: {:server_closed_request, _}}} ->
|
||||
true
|
||||
|
||||
_ ->
|
||||
false
|
||||
end)
|
||||
|
||||
refute {:done, request_ref} in responses
|
||||
refute :timeout in responses
|
||||
end
|
||||
end
|
||||
|
||||
test "resets an incomplete Bandit HTTP/2 stream" do
|
||||
Mox.set_mox_global(ClientMock)
|
||||
stream_then_error_mock("/bandit-http2-closed", [{"content-type", "image/png"}])
|
||||
|
||||
pid = start_bandit("/bandit-http2-closed")
|
||||
{:ok, {{127, 0, 0, 1}, port}} = ThousandIsland.listener_info(pid)
|
||||
{:ok, mint} = Mint.HTTP2.connect(:http, "localhost", port)
|
||||
{:ok, mint, request_ref} = Mint.HTTP2.request(mint, "GET", "/", [], nil)
|
||||
responses = receive_mint_responses(mint)
|
||||
|
||||
assert {:status, request_ref, 200} in responses
|
||||
assert {:data, request_ref, "partial"} in responses
|
||||
|
||||
assert Enum.any?(responses, fn
|
||||
{:error, ^request_ref,
|
||||
%Mint.HTTPError{reason: {:server_closed_request, :internal_error}}} ->
|
||||
true
|
||||
|
||||
_ ->
|
||||
false
|
||||
end)
|
||||
|
||||
refute {:done, request_ref} in responses
|
||||
refute :timeout in responses
|
||||
end
|
||||
|
||||
defp stream_mock(invokes) do
|
||||
|
|
@ -247,9 +565,14 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
test "max_body_length returns error if streaming body more than that option", %{conn: conn} do
|
||||
stream_mock(3)
|
||||
|
||||
assert capture_log(fn ->
|
||||
ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 29)
|
||||
end) =~
|
||||
log =
|
||||
capture_log(fn ->
|
||||
assert_raise ReverseProxy.StreamError, fn ->
|
||||
ReverseProxy.call(conn, "/stream-bytes/50", max_body_length: 29)
|
||||
end
|
||||
end)
|
||||
|
||||
assert log =~
|
||||
"Elixir.Pleroma.ReverseProxy request to /stream-bytes/50 failed while reading/chunking: :body_too_large"
|
||||
|
||||
assert_received {:closed, %{url: "/stream-bytes/50"}}
|
||||
|
|
@ -277,6 +600,19 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
assert Conn.get_resp_header(conn, "content-type") == ["image/png"]
|
||||
assert conn.resp_body == ""
|
||||
end
|
||||
|
||||
test "rejects a malformed content-length", %{conn: conn} do
|
||||
url = "/head-invalid-content-length"
|
||||
|
||||
expect(ClientMock, :request, fn :head, ^url, _, _, _ ->
|
||||
{:ok, 200, [{"content-length", "20junk"}]}
|
||||
end)
|
||||
|
||||
conn = ReverseProxy.call(Map.put(conn, :method, "HEAD"), url)
|
||||
|
||||
assert conn.status == 500
|
||||
assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
|
||||
end
|
||||
end
|
||||
|
||||
defp error_mock(status) when is_integer(status) do
|
||||
|
|
@ -351,34 +687,6 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
assert Conn.get_resp_header(conn, "content-type") == ["application/octet-stream"]
|
||||
end
|
||||
|
||||
test "sniffs and preserves an image body with a generic content type", %{conn: conn} do
|
||||
body = File.read!("test/fixtures/image.jpg")
|
||||
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/extensionless", headers, _, _ ->
|
||||
assert {"accept-encoding", "identity"} in headers
|
||||
{:ok, 200, [{"content-type", "application/octet-stream"}], %{body: body}}
|
||||
end)
|
||||
|> expect(:stream_body, fn %{body: ^body} = client ->
|
||||
{:ok, body, Map.delete(client, :body)}
|
||||
end)
|
||||
|> expect(:stream_body, fn %{} -> :done end)
|
||||
|
||||
conn =
|
||||
ReverseProxy.call(conn, "/extensionless",
|
||||
sniff_content_type: true,
|
||||
max_read_duration: :infinity,
|
||||
req_headers: [{"accept-encoding", "gzip"}]
|
||||
)
|
||||
|
||||
assert conn.resp_body == body
|
||||
assert Conn.get_resp_header(conn, "content-type") == ["image/jpeg"]
|
||||
|
||||
assert Conn.get_resp_header(conn, "content-disposition") == [
|
||||
"inline; filename=\"inline.jpg\""
|
||||
]
|
||||
end
|
||||
|
||||
test "preserves a generic content type for a non-image body", %{conn: conn} do
|
||||
body = "not an image"
|
||||
|
||||
|
|
@ -473,7 +781,73 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
|> expect(:stream_body, fn _ -> :done end)
|
||||
|
||||
conn = ReverseProxy.call(conn, "/cache")
|
||||
assert {"cache-control", "public, max-age=1209600, immutable"} in conn.resp_headers
|
||||
|
||||
assert {"cache-control", "public, max-age=1209600, immutable, no-transform"} in conn.resp_headers
|
||||
end
|
||||
|
||||
test "preserves a valid upstream content-length", %{conn: conn} do
|
||||
body = "complete body"
|
||||
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, "/content-length", _, _, _ ->
|
||||
{:ok, 200, [{"content-length", to_string(byte_size(body))}], %{body: body}}
|
||||
end)
|
||||
|> expect(:stream_body, fn %{body: ^body} = client ->
|
||||
{:ok, body, Map.delete(client, :body)}
|
||||
end)
|
||||
|> expect(:stream_body, fn %{} -> :done end)
|
||||
|
||||
conn = ReverseProxy.call(conn, "/content-length")
|
||||
|
||||
assert Conn.get_resp_header(conn, "content-length") == [to_string(byte_size(body))]
|
||||
end
|
||||
|
||||
test "rejects malformed upstream content-length values" do
|
||||
Enum.each(["20junk", "-1"], fn content_length ->
|
||||
url = "/invalid-content-length/#{content_length}"
|
||||
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, ^url, _, _, _ ->
|
||||
{:ok, 200, [{"content-length", content_length}], %{url: url}}
|
||||
end)
|
||||
|> expect(:close, fn %{url: ^url} -> :ok end)
|
||||
|
||||
conn = ReverseProxy.call(build_conn(), url)
|
||||
|
||||
assert conn.status == 500
|
||||
assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
|
||||
end)
|
||||
end
|
||||
|
||||
test "rejects conflicting upstream content-length values" do
|
||||
url = "/conflicting-content-length"
|
||||
|
||||
ClientMock
|
||||
|> expect(:request, fn :get, ^url, _, _, _ ->
|
||||
{:ok, 200, [{"content-length", "7"}, {"content-length", "20"}], %{url: url}}
|
||||
end)
|
||||
|> stub(:stream_body, fn _ -> :done end)
|
||||
|> stub(:close, fn _ -> :ok end)
|
||||
|
||||
conn = ReverseProxy.call(build_conn(), url)
|
||||
|
||||
assert conn.status == 500
|
||||
assert Cachex.get(:failed_proxy_url_cache, url) == {:ok, true}
|
||||
end
|
||||
|
||||
test "normalizes equivalent upstream content-length values", %{conn: conn} do
|
||||
body = "partial"
|
||||
|
||||
complete_stream_mock(
|
||||
"/duplicate-content-length",
|
||||
[{"content-length", "7"}, {"content-length", "07"}],
|
||||
body
|
||||
)
|
||||
|
||||
conn = ReverseProxy.call(conn, "/duplicate-content-length")
|
||||
|
||||
assert Conn.get_resp_header(conn, "content-length") == ["7"]
|
||||
assert conn.resp_body == body
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -507,24 +881,6 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
conn = ReverseProxy.call(conn, "/disposition")
|
||||
|
||||
assert {"content-type", "image/gif"} in conn.resp_headers
|
||||
assert {"content-disposition", "inline; filename=\"inline.gif\""} in conn.resp_headers
|
||||
end
|
||||
|
||||
test "forces inline for inline content types overriding upstream attachment", %{
|
||||
conn: conn
|
||||
} do
|
||||
disposition_headers_mock([
|
||||
{"content-type", "image/png"},
|
||||
{"content-disposition", "attachment; filename=\"filename.png\""},
|
||||
{"content-disposition", "attachment; filename=\"duplicate.png\""},
|
||||
{"content-length", "0"}
|
||||
])
|
||||
|
||||
conn = ReverseProxy.call(conn, "/disposition")
|
||||
|
||||
[disposition] = Conn.get_resp_header(conn, "content-disposition")
|
||||
assert String.starts_with?(disposition, "inline")
|
||||
refute String.starts_with?(disposition, "attachment")
|
||||
end
|
||||
|
||||
test "with content-disposition header", %{conn: conn} do
|
||||
|
|
@ -537,50 +893,6 @@ defmodule Pleroma.ReverseProxyTest do
|
|||
|
||||
assert {"content-disposition", "attachment; filename=\"filename.jpg\""} in conn.resp_headers
|
||||
end
|
||||
|
||||
test "with inline_content_types: true leaves upstream headers untouched", %{
|
||||
conn: conn
|
||||
} do
|
||||
# opt == true: the proxy must not synthesise or rewrite content-disposition.
|
||||
disposition_headers_mock([
|
||||
{"content-type", "image/png"},
|
||||
{"content-disposition", "attachment; filename=\"upstream.png\""},
|
||||
{"content-length", "0"}
|
||||
])
|
||||
|
||||
conn = ReverseProxy.call(conn, "/disposition", inline_content_types: true)
|
||||
|
||||
assert {"content-disposition", "attachment; filename=\"upstream.png\""} in conn.resp_headers
|
||||
end
|
||||
|
||||
test "forces bare inline for a whitelisted type with no MIME extension", %{
|
||||
conn: conn
|
||||
} do
|
||||
# image/x-foo-bar has no entry in MIME's database, so inline_filename/1
|
||||
# returns nil and the disposition should be the bare token "inline".
|
||||
disposition_headers_mock([
|
||||
{"content-type", "image/x-foo-bar"},
|
||||
{"content-length", "0"}
|
||||
])
|
||||
|
||||
conn = ReverseProxy.call(conn, "/disposition", inline_content_types: ["image/x-foo-bar"])
|
||||
|
||||
[disposition] = Conn.get_resp_header(conn, "content-disposition")
|
||||
assert disposition == "inline"
|
||||
end
|
||||
|
||||
test "serves modern browser image types inline", %{conn: conn} do
|
||||
disposition_headers_mock([
|
||||
{"content-type", "image/webp"},
|
||||
{"content-length", "0"}
|
||||
])
|
||||
|
||||
conn = ReverseProxy.call(conn, "/disposition")
|
||||
|
||||
assert Conn.get_resp_header(conn, "content-disposition") == [
|
||||
"inline; filename=\"inline.webp\""
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
describe "content-type sanitisation" do
|
||||
|
|
|
|||
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
|
||||
|
|
|
|||
|
|
@ -11,12 +11,14 @@ defmodule Pleroma.UserTest do
|
|||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Webhook.Notify
|
||||
|
||||
use Pleroma.DataCase, async: false
|
||||
use Oban.Testing, repo: Pleroma.Repo
|
||||
|
||||
import Pleroma.Factory
|
||||
import ExUnit.CaptureLog
|
||||
import Mock
|
||||
import Swoosh.TestAssertions
|
||||
|
||||
setup do
|
||||
|
|
@ -719,6 +721,14 @@ defmodule Pleroma.UserTest do
|
|||
|
||||
assert user.is_confirmed
|
||||
end
|
||||
|
||||
test_with_mock "triggers webhooks", Notify, trigger_webhooks: fn _, _ -> nil end do
|
||||
cng = User.register_changeset(%User{}, @full_user_data)
|
||||
|
||||
{:ok, registered_user} = User.register(cng)
|
||||
|
||||
assert_called(Notify.trigger_webhooks(registered_user, :"account.created"))
|
||||
end
|
||||
end
|
||||
|
||||
describe "user registration, with :account_activation_required" do
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.AdminAPI.AccountView
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Webhook.Notify
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
import Mock
|
||||
|
|
@ -1877,6 +1878,29 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
|||
assert Repo.aggregate(Object, :count, :id) == 1
|
||||
assert Repo.aggregate(Notification, :count, :id) == 0
|
||||
end
|
||||
|
||||
test_with_mock "triggers webhooks",
|
||||
%{
|
||||
reporter: reporter,
|
||||
context: context,
|
||||
target_account: target_account,
|
||||
reported_activity: reported_activity,
|
||||
content: content
|
||||
},
|
||||
Notify,
|
||||
[:passthrough],
|
||||
trigger_webhooks: fn _, _ -> nil end do
|
||||
{:ok, activity} =
|
||||
ActivityPub.flag(%{
|
||||
actor: reporter,
|
||||
context: context,
|
||||
account: target_account,
|
||||
statuses: [reported_activity],
|
||||
content: content
|
||||
})
|
||||
|
||||
assert_called(Notify.trigger_webhooks(activity, :"report.created"))
|
||||
end
|
||||
end
|
||||
|
||||
test "fetch_activities/2 returns activities addressed to a list " do
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -121,6 +121,19 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "effective configuration omits static search settings", %{conn: conn} do
|
||||
insert(:config, key: Pleroma.Search, value: [module: Pleroma.Search.ParadeDB])
|
||||
insert(:config, key: Pleroma.Search.ParadeDB, value: [url: "postgres://example/db"])
|
||||
|
||||
%{"configs" => configs} =
|
||||
conn
|
||||
|> get("/api/pleroma/admin/config")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
refute Enum.any?(configs, &(&1["key"] == "Pleroma.Search"))
|
||||
refute Enum.any?(configs, &(&1["key"] == "Pleroma.Search.ParadeDB"))
|
||||
end
|
||||
|
||||
test "subkeys with full update right merge", %{conn: conn} do
|
||||
insert(:config,
|
||||
key: ":emoji",
|
||||
|
|
@ -1552,12 +1565,15 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
|
|||
assert web_endpoint["children"]
|
||||
end
|
||||
|
||||
test "all keys from description are whitelisted", %{conn: conn} do
|
||||
test "descriptions exclude blacklisted keys", %{conn: conn} do
|
||||
conn = get(conn, "/api/pleroma/admin/config/descriptions")
|
||||
|
||||
assert response = json_response_and_validate_schema(conn, 200)
|
||||
|
||||
assert length(response) == length(Pleroma.Docs.JSON.compiled_descriptions())
|
||||
refute Enum.any?(response, &(&1["key"] == "Pleroma.Search"))
|
||||
refute Enum.any?(response, &(&1["key"] == "Pleroma.Search.ParadeDB"))
|
||||
|
||||
assert length(response) == length(Pleroma.Docs.JSON.compiled_descriptions()) - 2
|
||||
end
|
||||
|
||||
test "filters by database configuration blacklist and whitelist", %{conn: conn} do
|
||||
|
|
|
|||
|
|
@ -176,6 +176,183 @@ defmodule Pleroma.Web.AdminAPI.UserControllerTest do
|
|||
assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == []
|
||||
end
|
||||
|
||||
test "Admin-created users bypass account confirmation", %{conn: conn} do
|
||||
clear_config([:instance, :account_activation_required], true)
|
||||
|
||||
conn
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/pleroma/admin/users", %{
|
||||
"users" => [
|
||||
%{"nickname" => "lain", "email" => "lain@example.org", "password" => "test"}
|
||||
]
|
||||
})
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert User.get_by_nickname("lain").is_confirmed
|
||||
end
|
||||
|
||||
test "Creating without a password returns a password reset link", %{conn: conn} do
|
||||
accounts =
|
||||
conn
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/pleroma/admin/users", %{
|
||||
"users" => [
|
||||
%{
|
||||
"nickname" => "lain",
|
||||
"email" => "lain@example.org"
|
||||
},
|
||||
%{
|
||||
"nickname" => "lain2",
|
||||
"email" => "lain2@example.org",
|
||||
"password" => "dupa.8"
|
||||
}
|
||||
]
|
||||
})
|
||||
|> json_response_and_validate_schema(200)
|
||||
|> Map.new(fn account -> {account["data"]["nickname"], account} end)
|
||||
|
||||
account1 = accounts["lain"]
|
||||
account2 = accounts["lain2"]
|
||||
|
||||
assert account1["type"] == "success"
|
||||
assert %{"password_reset_link" => link} = account1["data"]
|
||||
assert link =~ "/api/v1/pleroma/password_reset/"
|
||||
|
||||
assert account2["type"] == "success"
|
||||
refute Map.has_key?(account2["data"], "password_reset_link")
|
||||
end
|
||||
|
||||
test "Explicit empty passwords return a conflict", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/pleroma/admin/users", %{
|
||||
"users" => [
|
||||
%{"nickname" => "lain", "email" => "lain@example.org", "password" => ""}
|
||||
]
|
||||
})
|
||||
|
||||
assert [%{"error" => "password can't be blank"}] = json_response(conn, 409)
|
||||
refute User.get_by_nickname("lain")
|
||||
end
|
||||
|
||||
test "Password reset token failures roll back the user batch", %{conn: conn} do
|
||||
idempotency_key = Ecto.UUID.generate()
|
||||
|
||||
request = fn conn ->
|
||||
conn
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> put_req_header("idempotency-key", idempotency_key)
|
||||
|> post("/api/pleroma/admin/users", %{
|
||||
"users" => [
|
||||
%{"nickname" => "lain", "email" => "lain@example.org"},
|
||||
%{"nickname" => "lain2", "email" => "lain2@example.org"}
|
||||
]
|
||||
})
|
||||
end
|
||||
|
||||
with_mock Pleroma.PasswordResetToken, [:passthrough],
|
||||
create_token: fn user ->
|
||||
if user.nickname == "lain2" do
|
||||
{:error, :forced}
|
||||
else
|
||||
result = passthrough([user])
|
||||
send(self(), {:token_created, user.nickname})
|
||||
result
|
||||
end
|
||||
end do
|
||||
conn = request.(conn)
|
||||
|
||||
assert json_response(conn, 500) == %{"error" => "Something went wrong"}
|
||||
end
|
||||
|
||||
assert_received {:token_created, "lain"}
|
||||
refute User.get_by_nickname("lain")
|
||||
refute User.get_by_nickname("lain2")
|
||||
assert Repo.aggregate(Pleroma.PasswordResetToken, :count) == 0
|
||||
|
||||
assert [_account1, _account2] = conn |> request.() |> json_response(200)
|
||||
end
|
||||
|
||||
test "Password reset links are not cached by idempotency key", %{conn: conn} do
|
||||
idempotency_key = Ecto.UUID.generate()
|
||||
|
||||
request = fn conn ->
|
||||
conn
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> put_req_header("idempotency-key", idempotency_key)
|
||||
|> post("/api/pleroma/admin/users", %{
|
||||
"users" => [%{"nickname" => "lain", "email" => "lain@example.org"}]
|
||||
})
|
||||
end
|
||||
|
||||
assert [%{"data" => %{"password_reset_link" => _link}}] =
|
||||
conn |> request.() |> json_response(200)
|
||||
|
||||
second_conn = request.(conn)
|
||||
|
||||
assert [_error] = json_response(second_conn, 409)
|
||||
refute get_resp_header(second_conn, "idempotent-replayed") == ["true"]
|
||||
end
|
||||
|
||||
test "Explicit-password responses remain idempotent", %{conn: conn} do
|
||||
idempotency_key = Ecto.UUID.generate()
|
||||
|
||||
request = fn conn ->
|
||||
conn
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> put_req_header("idempotency-key", idempotency_key)
|
||||
|> post("/api/pleroma/admin/users", %{
|
||||
"users" => [
|
||||
%{"nickname" => "lain", "email" => "lain@example.org", "password" => "test"}
|
||||
]
|
||||
})
|
||||
end
|
||||
|
||||
assert [_account] = conn |> request.() |> json_response(200)
|
||||
|
||||
second_conn = request.(conn)
|
||||
|
||||
assert [_account] = json_response(second_conn, 200)
|
||||
assert get_resp_header(second_conn, "idempotent-replayed") == ["true"]
|
||||
end
|
||||
|
||||
test "Duplicate nicknames in a batch return a conflict", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/pleroma/admin/users", %{
|
||||
"users" => [
|
||||
%{"nickname" => "lain", "email" => "lain1@example.org", "password" => "test"},
|
||||
%{"nickname" => "lain", "email" => "lain2@example.org", "password" => "test"}
|
||||
]
|
||||
})
|
||||
|
||||
assert [_first, _second] = json_response(conn, 409)
|
||||
refute User.get_by_nickname("lain")
|
||||
end
|
||||
|
||||
test "Blank nicknames return a conflict", %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/pleroma/admin/users", %{
|
||||
"users" => [
|
||||
%{"nickname" => "", "email" => "lain@example.org", "password" => "test"}
|
||||
]
|
||||
})
|
||||
|
||||
assert [%{"error" => "nickname can't be blank"}] = json_response(conn, 409)
|
||||
end
|
||||
|
||||
test "Cannot create user with existing email", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.AdminAPI.WebhookControllerTest do
|
||||
use Pleroma.Web.ConnCase, async: false
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
alias Pleroma.Webhook
|
||||
|
||||
setup do
|
||||
admin = insert(:user, is_admin: true)
|
||||
token = insert(:oauth_admin_token, user: admin)
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|> assign(:user, admin)
|
||||
|> assign(:token, token)
|
||||
|
||||
{:ok, %{admin: admin, token: token, conn: conn}}
|
||||
end
|
||||
|
||||
describe "GET /api/pleroma/admin/webhook" do
|
||||
test "lists existing webhooks", %{conn: conn} do
|
||||
Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]})
|
||||
Webhook.create(%{url: "https://example.com/webhook2", events: [:"account.created"]})
|
||||
|
||||
response =
|
||||
conn
|
||||
|> get("/api/pleroma/admin/webhooks")
|
||||
|> json_response_and_validate_schema(:ok)
|
||||
|
||||
assert length(response) == 2
|
||||
end
|
||||
|
||||
test "requires an admin even if a moderator can manage announcements" do
|
||||
clear_config([:instance, :moderator_privileges], [:announcements_manage_announcements])
|
||||
|
||||
moderator = insert(:user, is_moderator: true)
|
||||
token = insert(:oauth_admin_token, user: moderator)
|
||||
|
||||
build_conn()
|
||||
|> assign(:user, moderator)
|
||||
|> assign(:token, token)
|
||||
|> get("/api/pleroma/admin/webhooks")
|
||||
|> json_response(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /api/pleroma/admin/webhooks" do
|
||||
test "creates a webhook", %{conn: conn} do
|
||||
%{"id" => id} =
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/pleroma/admin/webhooks", %{
|
||||
url: "http://example.com/webhook",
|
||||
events: ["account.created"]
|
||||
})
|
||||
|> json_response_and_validate_schema(:ok)
|
||||
|
||||
assert %{url: "http://example.com/webhook", events: [:"account.created"]} = Webhook.get(id)
|
||||
end
|
||||
|
||||
test "rejects empty events", %{conn: conn} do
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/pleroma/admin/webhooks", %{
|
||||
url: "http://example.com/webhook",
|
||||
events: []
|
||||
})
|
||||
|> json_response(:bad_request)
|
||||
end
|
||||
|
||||
test "rejects internal flag", %{conn: conn} do
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> post("/api/pleroma/admin/webhooks", %{
|
||||
url: "http://example.com/webhook",
|
||||
events: ["account.created"],
|
||||
internal: true
|
||||
})
|
||||
|> json_response(:bad_request)
|
||||
end
|
||||
end
|
||||
|
||||
describe "PATCH /api/pleroma/admin/webhooks" do
|
||||
test "edits a webhook", %{conn: conn} do
|
||||
{:ok, %{id: id}} =
|
||||
Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]})
|
||||
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> patch("/api/pleroma/admin/webhooks/#{id}", %{
|
||||
events: ["report.created", "account.created"]
|
||||
})
|
||||
|> json_response_and_validate_schema(:ok)
|
||||
|
||||
assert %{events: [:"report.created", :"account.created"]} = Webhook.get(id)
|
||||
end
|
||||
|
||||
test "rejects empty events", %{conn: conn} do
|
||||
{:ok, %{id: id}} =
|
||||
Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]})
|
||||
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> patch("/api/pleroma/admin/webhooks/#{id}", %{events: []})
|
||||
|> json_response(:bad_request)
|
||||
|
||||
assert %{events: [:"report.created"]} = Webhook.get(id)
|
||||
end
|
||||
|
||||
test "rejects internal flag", %{conn: conn} do
|
||||
{:ok, %{id: id}} =
|
||||
Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]})
|
||||
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> patch("/api/pleroma/admin/webhooks/#{id}", %{internal: true})
|
||||
|> json_response(:bad_request)
|
||||
|
||||
assert %{internal: false} = Webhook.get(id)
|
||||
end
|
||||
|
||||
test "can't edit an internal webhook", %{conn: conn} do
|
||||
{:ok, %{id: id}} =
|
||||
Webhook.create(%{url: "https://example.com/webhook1", events: [], internal: true})
|
||||
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> patch("/api/pleroma/admin/webhooks/#{id}", %{
|
||||
events: ["report.created", "account.created"]
|
||||
})
|
||||
|> json_response_and_validate_schema(:forbidden)
|
||||
|
||||
assert %{events: []} = Webhook.get(id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE /api/pleroma/admin/webhooks" do
|
||||
test "deletes a webhook", %{conn: conn} do
|
||||
{:ok, %{id: id}} =
|
||||
Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]})
|
||||
|
||||
conn
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> delete("/api/pleroma/admin/webhooks/#{id}")
|
||||
|> json_response_and_validate_schema(:ok)
|
||||
|
||||
assert [] =
|
||||
Webhook
|
||||
|> Pleroma.Repo.all()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -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
|
||||
|
|
@ -458,6 +458,170 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
|
|||
assert [%{"id" => ^image_post_id}] = json_response_and_validate_schema(conn, 200)
|
||||
end
|
||||
|
||||
test "media respects activity restrictions when unauthenticated profiles are enabled", %{
|
||||
conn: conn
|
||||
} do
|
||||
clear_config([:instance, :public], false)
|
||||
clear_config([:restrict_unauthenticated, :profiles, :local], false)
|
||||
|
||||
user = insert(:user)
|
||||
|
||||
file = %Plug.Upload{
|
||||
content_type: "image/jpeg",
|
||||
path: Path.absname("test/fixtures/image.jpg"),
|
||||
filename: "an_image.jpg"
|
||||
}
|
||||
|
||||
{:ok, %{id: media_id}} = ActivityPub.upload(file, actor: user.ap_id)
|
||||
{:ok, activity} = CommonAPI.post(user, %{status: "cofe", media_ids: [media_id]})
|
||||
user_id = user.id
|
||||
|
||||
assert %{"id" => ^user_id} =
|
||||
build_conn()
|
||||
|> get("/api/v1/accounts/#{user.id}")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert [] =
|
||||
build_conn()
|
||||
|> get("/api/v1/accounts/#{user.id}/statuses?only_media=true")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert [%{"id" => activity_id}] =
|
||||
conn
|
||||
|> get("/api/v1/accounts/#{user.id}/statuses?only_media=true")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert activity_id == activity.id
|
||||
end
|
||||
|
||||
test "remote activity restrictions apply to created and boosted objects", %{conn: conn} do
|
||||
clear_config([:restrict_unauthenticated, :activities, :local], false)
|
||||
clear_config([:restrict_unauthenticated, :activities, :remote], true)
|
||||
|
||||
local_user = insert(:user)
|
||||
remote_user = insert(:user, local: false, domain: "example.com")
|
||||
local_activity = insert(:note_activity, user: local_user)
|
||||
|
||||
mixed_create =
|
||||
insert(:note_activity, local: true, object_local: false, user: local_user)
|
||||
|
||||
remote_activity =
|
||||
insert(:note_activity, local: false, object_local: false, user: remote_user)
|
||||
|
||||
{:ok, announce} = CommonAPI.repeat(remote_activity.id, local_user)
|
||||
|
||||
status_ids =
|
||||
build_conn()
|
||||
|> get("/api/v1/accounts/#{local_user.id}/statuses")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|> Enum.map(& &1["id"])
|
||||
|
||||
assert local_activity.id in status_ids
|
||||
refute mixed_create.id in status_ids
|
||||
refute announce.id in status_ids
|
||||
|
||||
authenticated_status_ids =
|
||||
conn
|
||||
|> get("/api/v1/accounts/#{local_user.id}/statuses")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|> Enum.map(& &1["id"])
|
||||
|
||||
assert local_activity.id in authenticated_status_ids
|
||||
assert mixed_create.id in authenticated_status_ids
|
||||
assert announce.id in authenticated_status_ids
|
||||
end
|
||||
|
||||
test "local activity restrictions apply to created and boosted objects", %{conn: conn} do
|
||||
clear_config([:restrict_unauthenticated, :activities, :local], true)
|
||||
clear_config([:restrict_unauthenticated, :activities, :remote], false)
|
||||
|
||||
local_user = insert(:user)
|
||||
remote_user = insert(:user, local: false, domain: "example.com")
|
||||
local_activity = insert(:note_activity, user: local_user)
|
||||
|
||||
remote_activity =
|
||||
insert(:note_activity, local: false, object_local: false, user: remote_user)
|
||||
|
||||
mixed_create =
|
||||
insert(:note_activity, local: false, object_local: true, user: remote_user)
|
||||
|
||||
remote_announce =
|
||||
insert(:announce_activity, note_activity: local_activity, user: remote_user)
|
||||
|> Ecto.Changeset.change(%{
|
||||
local: false,
|
||||
recipients: ["https://www.w3.org/ns/activitystreams#Public"]
|
||||
})
|
||||
|> Repo.update!()
|
||||
|
||||
status_ids =
|
||||
build_conn()
|
||||
|> get("/api/v1/accounts/#{remote_user.id}/statuses")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|> Enum.map(& &1["id"])
|
||||
|
||||
assert remote_activity.id in status_ids
|
||||
refute mixed_create.id in status_ids
|
||||
refute remote_announce.id in status_ids
|
||||
|
||||
authenticated_status_ids =
|
||||
conn
|
||||
|> get("/api/v1/accounts/#{remote_user.id}/statuses")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|> Enum.map(& &1["id"])
|
||||
|
||||
assert remote_activity.id in authenticated_status_ids
|
||||
assert mixed_create.id in authenticated_status_ids
|
||||
assert remote_announce.id in authenticated_status_ids
|
||||
end
|
||||
|
||||
test "restricted local accounts reject activities with mismatched remote origins", %{
|
||||
conn: conn
|
||||
} do
|
||||
clear_config([:restrict_unauthenticated, :activities, :local], true)
|
||||
clear_config([:restrict_unauthenticated, :activities, :remote], false)
|
||||
|
||||
local_user = insert(:user)
|
||||
|
||||
mismatched_activity =
|
||||
insert(:note_activity, local: false, object_local: false, user: local_user)
|
||||
|
||||
assert [] =
|
||||
build_conn()
|
||||
|> get("/api/v1/accounts/#{local_user.id}/statuses")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert [%{"id" => activity_id}] =
|
||||
conn
|
||||
|> get("/api/v1/accounts/#{local_user.id}/statuses")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert activity_id == mismatched_activity.id
|
||||
end
|
||||
|
||||
test "restricted remote accounts reject activities with mismatched local origins", %{
|
||||
conn: conn
|
||||
} do
|
||||
clear_config([:restrict_unauthenticated, :activities, :local], false)
|
||||
clear_config([:restrict_unauthenticated, :activities, :remote], true)
|
||||
|
||||
remote_user = insert(:user, local: false, domain: "example.com")
|
||||
|
||||
mismatched_activity =
|
||||
insert(:note_activity, local: true, object_local: true, user: remote_user)
|
||||
|
||||
assert [] =
|
||||
build_conn()
|
||||
|> get("/api/v1/accounts/#{remote_user.id}/statuses")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert [%{"id" => activity_id}] =
|
||||
conn
|
||||
|> get("/api/v1/accounts/#{remote_user.id}/statuses")
|
||||
|> json_response_and_validate_schema(200)
|
||||
|
||||
assert activity_id == mismatched_activity.id
|
||||
end
|
||||
|
||||
test "gets a user's statuses without reblogs", %{user: user, conn: conn} do
|
||||
{:ok, %{id: post_id}} = CommonAPI.post(user, %{status: "HI!!!"})
|
||||
{:ok, _} = CommonAPI.repeat(post_id, user)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,15 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
|
|||
}
|
||||
|
||||
test_notifications_rendering([notification], recipient, [expected])
|
||||
|
||||
grouped =
|
||||
NotificationView.render("grouped_index.json", %{
|
||||
notifications: [notification],
|
||||
for: recipient
|
||||
})
|
||||
|
||||
assert [group] = grouped.notification_groups
|
||||
assert group.chat_message == expected.chat_message
|
||||
end
|
||||
|
||||
test "Mention notification" do
|
||||
|
|
@ -182,6 +191,16 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
|
|||
}
|
||||
|
||||
test_notifications_rendering([notification], follower, [expected])
|
||||
|
||||
grouped =
|
||||
NotificationView.render("grouped_index.json", %{
|
||||
notifications: [notification],
|
||||
for: follower
|
||||
})
|
||||
|
||||
assert [group] = grouped.notification_groups
|
||||
assert group.target_id == new_user.id
|
||||
assert Enum.any?(grouped.accounts, &(&1.id == new_user.id))
|
||||
end
|
||||
|
||||
test "EmojiReact notification" do
|
||||
|
|
@ -210,6 +229,13 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
|
|||
}
|
||||
|
||||
test_notifications_rendering([notification], user, [expected])
|
||||
|
||||
grouped =
|
||||
NotificationView.render("grouped_index.json", %{notifications: [notification], for: user})
|
||||
|
||||
assert [group] = grouped.notification_groups
|
||||
assert group.emoji == expected.emoji
|
||||
assert group.emoji_url == expected.emoji_url
|
||||
end
|
||||
|
||||
test "EmojiReact custom emoji notification" do
|
||||
|
|
@ -295,6 +321,15 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
|
|||
}
|
||||
|
||||
test_notifications_rendering([notification], moderator_user, [expected])
|
||||
|
||||
grouped =
|
||||
NotificationView.render("grouped_index.json", %{
|
||||
notifications: [notification],
|
||||
for: moderator_user
|
||||
})
|
||||
|
||||
assert [group] = grouped.notification_groups
|
||||
assert group.report == expected.report
|
||||
end
|
||||
|
||||
test "Edit notification" do
|
||||
|
|
|
|||
|
|
@ -668,23 +668,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
|||
assert expected == StatusView.render("attachment.json", %{attachment: object})
|
||||
assert_schema(expected, "Attachment", api_spec)
|
||||
end
|
||||
|
||||
test "uses the ActivityStreams image type when the MIME type is generic" do
|
||||
object = %{
|
||||
"type" => "Image",
|
||||
"url" => [
|
||||
%{
|
||||
"mediaType" => "application/octet-stream",
|
||||
"href" => "https://example.com/extensionless"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
attachment = StatusView.render("attachment.json", %{attachment: object})
|
||||
|
||||
assert attachment.type == "image"
|
||||
assert attachment.pleroma.mime_type == "application/octet-stream"
|
||||
end
|
||||
end
|
||||
|
||||
test "put the url advertised in the Activity in to the url attribute" do
|
||||
|
|
@ -845,15 +828,31 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
|||
|
||||
fields = %{
|
||||
"url" => page_url,
|
||||
"site_name" => "Example site name",
|
||||
"type" => "photo",
|
||||
"title" => "Example website",
|
||||
"image" => page_url <> "/example.jpg",
|
||||
"description" => "Example description"
|
||||
"description" => "Example description",
|
||||
"author_name" => "Example author",
|
||||
"author_url" => page_url <> "/author",
|
||||
"provider_name" => "Example provider",
|
||||
"provider_url" => page_url <> "/provider",
|
||||
"html" => "<img src=\"#{page_url}/example.jpg\">",
|
||||
"width" => "1024",
|
||||
"height" => 768
|
||||
}
|
||||
|
||||
{:ok, card} = Card.create(page_url, fields)
|
||||
|
||||
assert match?(%{provider_name: "example.com"}, StatusView.render("card.json", card))
|
||||
assert %{
|
||||
type: "photo",
|
||||
author_name: "Example author",
|
||||
author_url: "https://example.com/author",
|
||||
provider_name: "Example provider",
|
||||
provider_url: "https://example.com/provider",
|
||||
html: "<img src=\"https://example.com/example.jpg\">",
|
||||
width: 1024,
|
||||
height: 768
|
||||
} = StatusView.render("card.json", card)
|
||||
end
|
||||
|
||||
test "a rich media card has all media proxied" do
|
||||
|
|
|
|||
|
|
@ -71,10 +71,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
|
|||
|
||||
test "it performs ReverseProxy.call with valid signature", %{conn: conn, url: url} do
|
||||
with_mock Pleroma.ReverseProxy,
|
||||
call: fn _conn, _url, opts ->
|
||||
assert opts[:sniff_content_type]
|
||||
%Conn{status: :success}
|
||||
end do
|
||||
call: fn _conn, _url, _opts -> %Conn{status: :success} end do
|
||||
assert %Conn{status: :success} = get(conn, url)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,7 +5,11 @@
|
|||
defmodule Pleroma.Web.OAuth.AppTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.MFA.Token, as: MFAToken
|
||||
alias Pleroma.Web.OAuth.App
|
||||
alias Pleroma.Web.OAuth.Authorization
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
alias Pleroma.Web.Push.Subscription
|
||||
import Pleroma.Factory
|
||||
|
||||
describe "get_or_make/2" do
|
||||
|
|
@ -57,29 +61,32 @@ defmodule Pleroma.Web.OAuth.AppTest do
|
|||
test "removes orphaned apps" do
|
||||
attrs = %{client_name: "Mastodon-Local", redirect_uris: "."}
|
||||
{:ok, %App{} = old_app} = App.get_or_make(attrs, ["write"])
|
||||
authorization = insert(:oauth_authorization, app: old_app)
|
||||
token = insert(:oauth_token, app: old_app)
|
||||
mfa_token = insert(:mfa_token, authorization: authorization)
|
||||
push_subscription = insert(:push_subscription, token: token)
|
||||
|
||||
# backdate the old app so it's within the threshold for being cleaned up
|
||||
one_hour_ago = DateTime.add(DateTime.utc_now(), -3600)
|
||||
user = insert(:user)
|
||||
attrs = %{client_name: "OldButValid", redirect_uris: ".", user_id: user.id}
|
||||
{:ok, %App{} = old_owned_app} = App.get_or_make(attrs, ["write"])
|
||||
|
||||
one_hour_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -3600)
|
||||
|
||||
{:ok, _} =
|
||||
"UPDATE apps SET inserted_at = $1, updated_at = $1 WHERE id = $2"
|
||||
|> Pleroma.Repo.query([one_hour_ago, old_app.id])
|
||||
"UPDATE apps SET inserted_at = $1, updated_at = $1 WHERE id IN ($2, $3)"
|
||||
|> Repo.query([one_hour_ago, old_app.id, old_owned_app.id])
|
||||
|
||||
# Create the new app after backdating the old one
|
||||
attrs = %{client_name: "PleromaFE", redirect_uris: "."}
|
||||
{:ok, %App{} = app} = App.get_or_make(attrs, ["write"])
|
||||
|
||||
# Ensure the new app has a recent timestamp
|
||||
now = DateTime.utc_now()
|
||||
|
||||
{:ok, _} =
|
||||
"UPDATE apps SET inserted_at = $1, updated_at = $1 WHERE id = $2"
|
||||
|> Pleroma.Repo.query([now, app.id])
|
||||
{:ok, %App{} = recent_app} = App.get_or_make(attrs, ["write"])
|
||||
|
||||
App.remove_orphans()
|
||||
|
||||
assert [returned_app] = Pleroma.Repo.all(App)
|
||||
assert returned_app.client_name == "PleromaFE"
|
||||
assert returned_app.id == app.id
|
||||
refute Repo.get(App, old_app.id)
|
||||
refute Repo.get(Authorization, authorization.id)
|
||||
refute Repo.get(Token, token.id)
|
||||
refute Repo.get(MFAToken, mfa_token.id)
|
||||
refute Repo.get(Subscription, push_subscription.id)
|
||||
assert Repo.get(App, recent_app.id)
|
||||
assert Repo.get(App, old_owned_app.id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -70,6 +70,139 @@ defmodule Pleroma.Web.Plugs.IdempotencyPlugTest do
|
|||
assert [^key] = Conn.get_resp_header(conn, "idempotency-key")
|
||||
end
|
||||
|
||||
test "does not cache responses marked as sensitive" do
|
||||
key = Ecto.UUID.generate()
|
||||
|
||||
:post
|
||||
|> conn("/cofe")
|
||||
|> put_req_header("idempotency-key", key)
|
||||
|> Conn.put_resp_header("x-request-id", "sensitive-request")
|
||||
|> Conn.put_resp_content_type("application/json")
|
||||
|> IdempotencyPlug.call([])
|
||||
|> Conn.put_private(:skip_idempotency_cache, true)
|
||||
|> Conn.send_resp(200, "secret")
|
||||
|
||||
conn =
|
||||
:post
|
||||
|> conn("/cofe")
|
||||
|> put_req_header("idempotency-key", key)
|
||||
|> Conn.put_resp_header("x-request-id", "second-request")
|
||||
|> Conn.put_resp_content_type("application/json")
|
||||
|> IdempotencyPlug.call([])
|
||||
|
||||
refute conn.halted
|
||||
refute conn.resp_body
|
||||
end
|
||||
|
||||
test "does not replay legacy unscoped cache entries" do
|
||||
key = Ecto.UUID.generate()
|
||||
record = {"legacy-request", "application/json", 200, "secret"}
|
||||
assert {:ok, true} = Cachex.put(:idempotency_cache, key, record)
|
||||
|
||||
conn =
|
||||
:post
|
||||
|> conn("/cofe")
|
||||
|> put_req_header("idempotency-key", key)
|
||||
|> Conn.put_resp_header("x-request-id", "new-request")
|
||||
|> Conn.put_resp_content_type("application/json")
|
||||
|> IdempotencyPlug.call([])
|
||||
|
||||
refute conn.halted
|
||||
refute conn.resp_body
|
||||
end
|
||||
|
||||
test "scopes cached responses to the route and authenticated actor" do
|
||||
key = Ecto.UUID.generate()
|
||||
|
||||
:post
|
||||
|> conn("/cofe")
|
||||
|> Conn.assign(:user, %{id: "user-one", is_admin: false, is_moderator: false})
|
||||
|> put_req_header("idempotency-key", key)
|
||||
|> Conn.put_resp_header("x-request-id", "original-request")
|
||||
|> Conn.put_resp_content_type("application/json")
|
||||
|> IdempotencyPlug.call([])
|
||||
|> Conn.send_resp(200, "one")
|
||||
|
||||
for {path, user} <- [
|
||||
{"/tea", %{id: "user-one", is_admin: false, is_moderator: false}},
|
||||
{"/cofe", %{id: "user-two", is_admin: false, is_moderator: false}},
|
||||
{"/cofe", %{id: "user-one", is_admin: true, is_moderator: false}}
|
||||
] do
|
||||
conn =
|
||||
:post
|
||||
|> conn(path)
|
||||
|> Conn.assign(:user, user)
|
||||
|> put_req_header("idempotency-key", key)
|
||||
|> Conn.put_resp_header("x-request-id", Ecto.UUID.generate())
|
||||
|> Conn.put_resp_content_type("application/json")
|
||||
|> IdempotencyPlug.call([])
|
||||
|
||||
refute conn.halted
|
||||
refute conn.resp_body
|
||||
end
|
||||
end
|
||||
|
||||
test "scopes cached responses to the OAuth application and token" do
|
||||
key = Ecto.UUID.generate()
|
||||
|
||||
:post
|
||||
|> conn("/cofe")
|
||||
|> Conn.assign(:app, %{id: "app-one"})
|
||||
|> Conn.assign(:token, %{id: "token-one", scopes: ["write"]})
|
||||
|> put_req_header("idempotency-key", key)
|
||||
|> Conn.put_resp_header("x-request-id", "original-request")
|
||||
|> Conn.put_resp_content_type("application/json")
|
||||
|> IdempotencyPlug.call([])
|
||||
|> Conn.send_resp(200, "one")
|
||||
|
||||
for {app, token} <- [
|
||||
{%{id: "app-two"}, %{id: "token-two", scopes: ["write"]}},
|
||||
{%{id: "app-one"}, %{id: "token-two", scopes: ["write"]}},
|
||||
{%{id: "app-one"}, %{id: "token-one", scopes: ["read"]}}
|
||||
] do
|
||||
conn =
|
||||
:post
|
||||
|> conn("/cofe")
|
||||
|> Conn.assign(:app, app)
|
||||
|> Conn.assign(:token, token)
|
||||
|> put_req_header("idempotency-key", key)
|
||||
|> Conn.put_resp_header("x-request-id", Ecto.UUID.generate())
|
||||
|> Conn.put_resp_content_type("application/json")
|
||||
|> IdempotencyPlug.call([])
|
||||
|
||||
refute conn.halted
|
||||
refute conn.resp_body
|
||||
end
|
||||
end
|
||||
|
||||
test "scopes cached responses to configured staff privileges" do
|
||||
key = Ecto.UUID.generate()
|
||||
clear_config([:instance, :admin_privileges], [:users_delete])
|
||||
|
||||
:post
|
||||
|> conn("/cofe")
|
||||
|> Conn.assign(:user, %{id: "admin", is_admin: true, is_moderator: false})
|
||||
|> put_req_header("idempotency-key", key)
|
||||
|> Conn.put_resp_header("x-request-id", "original-request")
|
||||
|> Conn.put_resp_content_type("application/json")
|
||||
|> IdempotencyPlug.call([])
|
||||
|> Conn.send_resp(200, "one")
|
||||
|
||||
clear_config([:instance, :admin_privileges], [])
|
||||
|
||||
conn =
|
||||
:post
|
||||
|> conn("/cofe")
|
||||
|> Conn.assign(:user, %{id: "admin", is_admin: true, is_moderator: false})
|
||||
|> put_req_header("idempotency-key", key)
|
||||
|> Conn.put_resp_header("x-request-id", "second-request")
|
||||
|> Conn.put_resp_content_type("application/json")
|
||||
|> IdempotencyPlug.call([])
|
||||
|
||||
refute conn.halted
|
||||
refute conn.resp_body
|
||||
end
|
||||
|
||||
test "passes conn downstream if idempotency is not present in headers" do
|
||||
orig_request_id = "test4"
|
||||
body = "testing"
|
||||
|
|
|
|||
125
test/pleroma/webhook/notify_test.exs
Normal file
125
test/pleroma/webhook/notify_test.exs
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Webhook.NotifyTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.Webhook
|
||||
alias Pleroma.Webhook.Notify
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
test "triggers a webhook for a report" do
|
||||
%{id: activity_id} = activity = insert(:report_activity)
|
||||
|
||||
url = "https://example.com/webhook"
|
||||
|
||||
Webhook.create(%{url: url, events: [:"report.created"]})
|
||||
|
||||
Tesla.Mock.mock(fn %{url: ^url, body: body} = _ ->
|
||||
report =
|
||||
body
|
||||
|> Jason.decode!()
|
||||
|> Map.get("object")
|
||||
|
||||
assert %{"id" => ^activity_id} = report
|
||||
|
||||
%Tesla.Env{status: 200, body: ""}
|
||||
end)
|
||||
|
||||
[job] = Notify.trigger_webhooks(activity, :"report.created")
|
||||
|
||||
Pleroma.Workers.WebhookWorker.perform(job)
|
||||
end
|
||||
|
||||
test "triggers a webhook for an account" do
|
||||
%{id: account_id} = user = insert(:user)
|
||||
|
||||
url = "https://example.com/webhook"
|
||||
|
||||
Webhook.create(%{url: url, events: [:"account.created"]})
|
||||
|
||||
Tesla.Mock.mock(fn %{url: ^url, body: body} = _ ->
|
||||
report =
|
||||
body
|
||||
|> Jason.decode!()
|
||||
|> Map.get("object")
|
||||
|
||||
assert %{"id" => ^account_id} = report
|
||||
|
||||
%Tesla.Env{status: 200, body: ""}
|
||||
end)
|
||||
|
||||
[job] = Notify.trigger_webhooks(user, :"account.created")
|
||||
|
||||
Pleroma.Workers.WebhookWorker.perform(job)
|
||||
end
|
||||
|
||||
test "notifies have a valid signature" do
|
||||
activity = insert(:report_activity)
|
||||
|
||||
{:ok, %{secret: secret} = webhook} =
|
||||
Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]})
|
||||
|
||||
Tesla.Mock.mock(fn %{url: "https://example.com/webhook", body: body, headers: headers} = _ ->
|
||||
{"X-Hub-Signature", "sha256=" <> signature} =
|
||||
Enum.find(headers, fn {key, _} -> key == "X-Hub-Signature" end)
|
||||
|
||||
assert signature == :crypto.mac(:hmac, :sha256, secret, body) |> Base.encode16(case: :lower)
|
||||
%Tesla.Env{status: 200, body: ""}
|
||||
end)
|
||||
|
||||
Notify.report_created(webhook, activity)
|
||||
end
|
||||
|
||||
test "returns an error for unsuccessful responses" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, webhook} =
|
||||
Webhook.create(%{url: "https://example.com/webhook", events: [:"account.created"]})
|
||||
|
||||
Tesla.Mock.mock(fn %{url: "https://example.com/webhook"} ->
|
||||
%Tesla.Env{status: 503, body: ""}
|
||||
end)
|
||||
|
||||
assert {:error, {:http_status, 503}} = Notify.account_created(webhook, user)
|
||||
end
|
||||
|
||||
test "accepts any successful response" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, webhook} =
|
||||
Webhook.create(%{url: "https://example.com/webhook", events: [:"account.created"]})
|
||||
|
||||
Tesla.Mock.mock(fn %{url: "https://example.com/webhook"} ->
|
||||
%Tesla.Env{status: 204, body: ""}
|
||||
end)
|
||||
|
||||
assert :ok = Notify.account_created(webhook, user)
|
||||
end
|
||||
|
||||
test "cancels queued deliveries when the webhook was deleted" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, webhook} =
|
||||
Webhook.create(%{url: "https://example.com/webhook", events: [:"account.created"]})
|
||||
|
||||
[job] = Notify.trigger_webhooks(user, :"account.created")
|
||||
Webhook.delete(webhook)
|
||||
|
||||
assert {:cancel, :not_found} = Pleroma.Workers.WebhookWorker.perform(job)
|
||||
end
|
||||
|
||||
test "cancels queued deliveries when the webhook was disabled" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, webhook} =
|
||||
Webhook.create(%{url: "https://example.com/webhook", events: [:"account.created"]})
|
||||
|
||||
[job] = Notify.trigger_webhooks(user, :"account.created")
|
||||
Webhook.set_enabled(webhook, false)
|
||||
|
||||
assert {:cancel, :disabled} = Pleroma.Workers.WebhookWorker.perform(job)
|
||||
end
|
||||
end
|
||||
94
test/pleroma/webhook_test.exs
Normal file
94
test/pleroma/webhook_test.exs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.WebhookTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.Webhook
|
||||
|
||||
test "creating a webhook" do
|
||||
{:ok, %{id: id}} =
|
||||
Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]})
|
||||
|
||||
assert %{url: "https://example.com/webhook"} = Webhook.get(id)
|
||||
end
|
||||
|
||||
test "editing a webhook" do
|
||||
{:ok, %{id: id} = webhook} =
|
||||
Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]})
|
||||
|
||||
Webhook.update(webhook, %{events: [:"account.created"]})
|
||||
|
||||
assert %{events: [:"account.created"]} = Webhook.get(id)
|
||||
end
|
||||
|
||||
test "rejects invalid events" do
|
||||
assert {:error, _changeset} =
|
||||
Webhook.create(%{url: "https://example.com/webhook", events: [:"status.created"]})
|
||||
|
||||
{:ok, webhook} =
|
||||
Webhook.create(%{url: "https://example.com/valid-webhook", events: [:"report.created"]})
|
||||
|
||||
assert {:error, _changeset} = Webhook.update(webhook, %{events: [:"status.created"]})
|
||||
end
|
||||
|
||||
test "rejects URLs longer than the database column" do
|
||||
url = "https://example.com/" <> String.duplicate("a", 256)
|
||||
|
||||
assert {:error, changeset} =
|
||||
Webhook.create(%{url: url, events: [:"report.created"]})
|
||||
|
||||
assert %{url: ["should be at most 255 character(s)"]} = errors_on(changeset)
|
||||
end
|
||||
|
||||
test "rejects empty events for non-internal webhooks" do
|
||||
assert {:error, changeset} = Webhook.create(%{url: "https://example.com/webhook", events: []})
|
||||
assert %{events: ["can't be blank"]} = errors_on(changeset)
|
||||
|
||||
{:ok, webhook} =
|
||||
Webhook.create(%{url: "https://example.com/valid-webhook", events: [:"report.created"]})
|
||||
|
||||
assert {:error, changeset} = Webhook.update(webhook, %{events: []})
|
||||
assert %{events: ["can't be blank"]} = errors_on(changeset)
|
||||
|
||||
assert {:ok, _webhook} =
|
||||
Webhook.create(%{
|
||||
url: "https://example.com/internal-webhook",
|
||||
events: [],
|
||||
internal: true
|
||||
})
|
||||
end
|
||||
|
||||
test "filter webhooks by type" do
|
||||
{:ok, %{id: id1}} =
|
||||
Webhook.create(%{url: "https://example.com/webhook1", events: [:"report.created"]})
|
||||
|
||||
{:ok, %{id: id2}} =
|
||||
Webhook.create(%{
|
||||
url: "https://example.com/webhook2",
|
||||
events: [:"account.created", :"report.created"]
|
||||
})
|
||||
|
||||
Webhook.create(%{url: "https://example.com/webhook3", events: [:"account.created"]})
|
||||
|
||||
assert [%{id: ^id1}, %{id: ^id2}] = Webhook.get_by_type(:"report.created")
|
||||
end
|
||||
|
||||
test "change webhook state" do
|
||||
{:ok, %{id: id, enabled: true} = webhook} =
|
||||
Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]})
|
||||
|
||||
Webhook.set_enabled(webhook, false)
|
||||
assert %{enabled: false} = Webhook.get(id)
|
||||
end
|
||||
|
||||
test "rotate webhook secrets" do
|
||||
{:ok, %{id: id, secret: secret} = webhook} =
|
||||
Webhook.create(%{url: "https://example.com/webhook", events: [:"report.created"]})
|
||||
|
||||
Webhook.rotate_secret(webhook)
|
||||
%{secret: new_secret} = Webhook.get(id)
|
||||
assert secret != new_secret
|
||||
end
|
||||
end
|
||||
|
|
@ -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