Merge remote-tracking branch 'origin/develop' into pr-7444-finish
This commit is contained in:
commit
7a741736ea
1544 changed files with 43219 additions and 9351 deletions
BIN
test/fixtures/DSCN0010.png
vendored
Normal file
BIN
test/fixtures/DSCN0010.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 744 KiB |
34
test/fixtures/bovine-bogus-public-note.json
vendored
Normal file
34
test/fixtures/bovine-bogus-public-note.json
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
"https://www.w3.org/ns/did/v1",
|
||||
"https://w3id.org/security/multikey/v1",
|
||||
{
|
||||
"Hashtag": "as:Hashtag"
|
||||
}
|
||||
],
|
||||
"attributedTo": "https://mymath.rocks/endpoints/SYn3cl_N4HAPfPHgo2x37XunLEmhV9LnxCggcYwyec0",
|
||||
"cc": [
|
||||
"https://mymath.rocks/endpoints/30zoCe7haKBEFolH4rbAmKj-t9_bG0c2X2kMQkJk5qY",
|
||||
"https://mastodon.social/users/nikclayton"
|
||||
],
|
||||
"content": "<blockquote class=\"h-quote\">\n<p>I note that mymath.rocks does not provide this information.</p>\n</blockquote>\n<p>I'm a big believer in "Do as I say, not as I <strong>did</strong>".</p>\n<p>I could give a long list of technical reasons, which boil down to: nodeinfo is pretty nonsensical with the way I write stuff.</p>\n<p>I think the above statement also addresses a main failure of the Fediverse. People, e.g. me, would love to fix stuff. Unfortunately, we lack the focus to address a lot of issues, e.g. nodeinfo sucks. So stuff gets done in a broken way.</p>\n<p>I think the main challenge the Fediverse has faced, and failed at, is avoiding the above situation. To continue the example, there is no way for somebody to say: Let's fix nodeinfo and people will follow their ideas.</p>\n",
|
||||
"id": "https://mymath.rocks/objects/2b89e564-30cf-4eeb-97ca-7e638a154026",
|
||||
"inReplyTo": "https://mastodon.social/users/nikclayton/statuses/115496665258618127",
|
||||
"likes": "https://mymath.rocks/objects/2b89e564-30cf-4eeb-97ca-7e638a154026/likes",
|
||||
"published": "2025-11-06T08:21:17.790Z",
|
||||
"replies": "https://mymath.rocks/objects/2b89e564-30cf-4eeb-97ca-7e638a154026/replies",
|
||||
"shares": "https://mymath.rocks/objects/2b89e564-30cf-4eeb-97ca-7e638a154026/shares",
|
||||
"source": {
|
||||
"content": "> I note that mymath.rocks does not provide this information.\n\nI'm a big believer in \"Do as I say, not as I __did__\".\n\nI could give a long list of technical reasons, which boil down to: nodeinfo is pretty nonsensical with the way I write stuff.\n\nI think the above statement also addresses a main failure of the Fediverse. People, e.g. me, would love to fix stuff. Unfortunately, we lack the focus to address a lot of issues, e.g. nodeinfo sucks. So stuff gets done in a broken way.\n\nI think the main challenge the Fediverse has faced, and failed at, is avoiding the above situation. To continue the example, there is no way for somebody to say: Let's fix nodeinfo and people will follow their ideas.",
|
||||
"mediaType": "text/markdown"
|
||||
},
|
||||
"tag": {
|
||||
"href": "https://mastodon.social/users/nikclayton",
|
||||
"name": "https://mastodon.social/users/nikclayton",
|
||||
"type": "Mention"
|
||||
},
|
||||
"to": "as:Public",
|
||||
"type": "Note"
|
||||
}
|
||||
BIN
test/fixtures/break_analyze.png
vendored
Normal file
BIN
test/fixtures/break_analyze.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 360 KiB |
112
test/fixtures/denniskoch.json
vendored
Normal file
112
test/fixtures/denniskoch.json
vendored
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{
|
||||
"Curve25519Key": "toot:Curve25519Key",
|
||||
"Device": "toot:Device",
|
||||
"Ed25519Key": "toot:Ed25519Key",
|
||||
"Ed25519Signature": "toot:Ed25519Signature",
|
||||
"EncryptedMessage": "toot:EncryptedMessage",
|
||||
"PropertyValue": "schema:PropertyValue",
|
||||
"alsoKnownAs": {
|
||||
"@id": "as:alsoKnownAs",
|
||||
"@type": "@id"
|
||||
},
|
||||
"cipherText": "toot:cipherText",
|
||||
"claim": {
|
||||
"@id": "toot:claim",
|
||||
"@type": "@id"
|
||||
},
|
||||
"deviceId": "toot:deviceId",
|
||||
"devices": {
|
||||
"@id": "toot:devices",
|
||||
"@type": "@id"
|
||||
},
|
||||
"discoverable": "toot:discoverable",
|
||||
"featured": {
|
||||
"@id": "toot:featured",
|
||||
"@type": "@id"
|
||||
},
|
||||
"featuredTags": {
|
||||
"@id": "toot:featuredTags",
|
||||
"@type": "@id"
|
||||
},
|
||||
"fingerprintKey": {
|
||||
"@id": "toot:fingerprintKey",
|
||||
"@type": "@id"
|
||||
},
|
||||
"focalPoint": {
|
||||
"@container": "@list",
|
||||
"@id": "toot:focalPoint"
|
||||
},
|
||||
"identityKey": {
|
||||
"@id": "toot:identityKey",
|
||||
"@type": "@id"
|
||||
},
|
||||
"indexable": "toot:indexable",
|
||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||
"memorial": "toot:memorial",
|
||||
"messageFranking": "toot:messageFranking",
|
||||
"messageType": "toot:messageType",
|
||||
"movedTo": {
|
||||
"@id": "as:movedTo",
|
||||
"@type": "@id"
|
||||
},
|
||||
"publicKeyBase64": "toot:publicKeyBase64",
|
||||
"schema": "http://schema.org#",
|
||||
"suspended": "toot:suspended",
|
||||
"toot": "http://joinmastodon.org/ns#",
|
||||
"value": "schema:value"
|
||||
}
|
||||
],
|
||||
"attachment": [
|
||||
{
|
||||
"name": "GitHub",
|
||||
"type": "PropertyValue",
|
||||
"value": "<a href=\"https://github.com/pxlrbt/\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\" translate=\"no\"><span class=\"invisible\">https://</span><span class=\"\">github.com/pxlrbt/</span><span class=\"invisible\"></span></a>"
|
||||
},
|
||||
{
|
||||
"name": "Discord",
|
||||
"type": "PropertyValue",
|
||||
"value": "pxlrbt#6029"
|
||||
}
|
||||
],
|
||||
"devices": "https://phpc.social/users/denniskoch/collections/devices",
|
||||
"discoverable": true,
|
||||
"endpoints": {
|
||||
"sharedInbox": "https://phpc.social/inbox"
|
||||
},
|
||||
"featured": "https://phpc.social/users/denniskoch/collections/featured",
|
||||
"featuredTags": "https://phpc.social/users/denniskoch/collections/tags",
|
||||
"followers": "https://phpc.social/users/denniskoch/followers",
|
||||
"following": "https://phpc.social/users/denniskoch/following",
|
||||
"icon": {
|
||||
"mediaType": "image/jpeg",
|
||||
"type": "Image",
|
||||
"url": "https://media.phpc.social/accounts/avatars/109/364/097/179/042/485/original/6e770c7b3f5ef72d.jpg"
|
||||
},
|
||||
"id": "https://phpc.social/users/denniskoch",
|
||||
"image": {
|
||||
"mediaType": "image/jpeg",
|
||||
"type": "Image",
|
||||
"url": "https://media.phpc.social/accounts/headers/109/364/097/179/042/485/original/709da24705260c04.jpg"
|
||||
},
|
||||
"inbox": "https://phpc.social/users/denniskoch/inbox",
|
||||
"indexable": true,
|
||||
"manuallyApprovesFollowers": false,
|
||||
"memorial": false,
|
||||
"name": "Dennis Koch",
|
||||
"outbox": "https://phpc.social/users/denniskoch/outbox",
|
||||
"preferredUsername": "denniskoch",
|
||||
"publicKey": {
|
||||
"id": "https://phpc.social/users/denniskoch#main-key",
|
||||
"owner": "https://phpc.social/users/denniskoch",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4dmcSlqLj18gPvuslkmt\nQTniZ8ybO4pgvMvPLYtBuTBUjo49vJ/8Sw6jB5zcKb1haqIdny7Rv/vY3kCdCXcP\nloh1I+jthEgqLT8JpZWGwLGwg9piFhrMGADmt3N8du7HfglzuZ8LlVpnZ8feCw7I\nS2ua/ZCxE47mI45Z3ed2kkFYKWopWWqFn2lan/1OyHrcFKtCvaVjRdvo0UUt2tgl\nvyJI4+zN8FnrCbsMtcbI5nSzfJIrOc4LeaGmLJh+0o2rwoOQZc2487XWbeyfhjsq\nPRBpYN7pfHWQDvzQIN075LHTf9zDFsm6+HqY7Zs5rYxr72rvcX7d9JcP6CasIosY\nqwIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"published": "2022-11-18T00:00:00Z",
|
||||
"summary": "<p>🧑💻 Full Stack Developer<br />🚀 Laravel, Filament, Livewire, Vue, Inertia<br />🌍 Germany</p>",
|
||||
"tag": [],
|
||||
"type": "Person",
|
||||
"url": "https://phpc.social/@denniskoch"
|
||||
}
|
||||
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"
|
||||
]
|
||||
}
|
||||
}
|
||||
76
test/fixtures/friendica-dislike-undo.json
vendored
Normal file
76
test/fixtures/friendica-dislike-undo.json
vendored
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{
|
||||
"Hashtag": "as:Hashtag",
|
||||
"PropertyValue": "schema:PropertyValue",
|
||||
"conversation": "ostatus:conversation",
|
||||
"dfrn": "http://purl.org/macgirvin/dfrn/1.0/",
|
||||
"diaspora": "https://diasporafoundation.org/ns/",
|
||||
"directMessage": "litepub:directMessage",
|
||||
"discoverable": "toot:discoverable",
|
||||
"featured": {
|
||||
"@id": "toot:featured",
|
||||
"@type": "@id"
|
||||
},
|
||||
"litepub": "http://litepub.social/ns#",
|
||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||
"ostatus": "http://ostatus.org#",
|
||||
"quoteUrl": "as:quoteUrl",
|
||||
"schema": "http://schema.org#",
|
||||
"sensitive": "as:sensitive",
|
||||
"toot": "http://joinmastodon.org/ns#",
|
||||
"value": "schema:value",
|
||||
"vcard": "http://www.w3.org/2006/vcard/ns#"
|
||||
}
|
||||
],
|
||||
"actor": "https://my-place.social/profile/vaartis",
|
||||
"cc": [
|
||||
"https://my-place.social/followers/vaartis"
|
||||
],
|
||||
"id": "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182/Undo",
|
||||
"instrument": {
|
||||
"id": "https://my-place.social/friendica",
|
||||
"name": "Friendica 'Interrupted Fern' 2024.12-1576",
|
||||
"type": "Application",
|
||||
"url": "https://my-place.social"
|
||||
},
|
||||
"object": {
|
||||
"actor": "https://my-place.social/profile/vaartis",
|
||||
"cc": [
|
||||
"https://my-place.social/followers/vaartis"
|
||||
],
|
||||
"diaspora:guid": "e599373b-1968-4b20-cd24-80d340160302",
|
||||
"diaspora:like": "{\"author\":\"vaartis@my-place.social\",\"guid\":\"e599373b-1968-4b20-cd24-80d340160302\",\"parent_guid\":\"cd36feba-c31f3ed3fd5c064a-17c31593\",\"parent_type\":\"Post\",\"positive\":\"false\",\"author_signature\":\"xR2zLJNfc9Nhx1n8LLMWM1kde12my4cqamIsrH\\/UntKzuDwO4DuHBL0fkFhgC\\/dylxm4HqsHD45MQbtwQCVGq6WhC96TrbMuYEK61HIO23dTr3m+qJVtfdH4wyhUNHgiiYPhZpkLDfnR1JiRWmFTlmZC8q8JEkOB5IQsrWia2eOR6IsqDcdKO\\/Urgns9\\/BdQi8KnchBKSEFc1iUtcOEruvhunKGyW5zI\\/Rltfdz3xGH8tlw+YlMXeWXPnqgOJ9GzNA0lwG4U421L6yylYagW7oxIznnBLB4bO46vYZbgXZV1hiI9ZyveHOinLMY1QkmTj5CNvnx3\\/VJwLovd0v+0Nr2vu\\/3ftbpBXc6L1bsNjlRqtsfwJlcgl+tH1DC4W8tKf+Y3tdtzVw0CHXCuacxHLyq5wZd\\/5YfYR9SJQ\\/jInU4PHA5+hIE3PGqNUp5QfFE0umq56H7MQKsIPgM5mMV4fPAA8OpltuMVDvQYUxalrnvoTf00k90x1wCTK71\\/jQGh7r7PmGvSdfPr+65eVTjITD8\\/lTGIb8850v1fl3\\/i2R8Dv17jQIRyX5o9MXPSO6jHo4Swma5RzPA\\/0bRj6qRTyPkM1L9qEIr+2H2I7KKhT2ZE5GhAU7yI9A3VLBWzpTrUPMGbfpd1OjVTEqXAdMjpLDYI3Mh5zQ58p8xCzt+W+t0=\"}",
|
||||
"id": "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182",
|
||||
"instrument": {
|
||||
"id": "https://my-place.social/friendica",
|
||||
"name": "Friendica 'Interrupted Fern' 2024.12-1576",
|
||||
"type": "Application",
|
||||
"url": "https://my-place.social"
|
||||
},
|
||||
"object": "https://pl.kotobank.ch/objects/301bce65-8a1b-4c49-a65c-fe2ce861a213",
|
||||
"published": "2025-06-12T18:47:41Z",
|
||||
"to": [
|
||||
"https://pl.kotobank.ch/users/vaartis",
|
||||
"https://mitra.social/users/silverpill",
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"type": "Dislike"
|
||||
},
|
||||
"published": "2025-06-12T18:41:25Z",
|
||||
"signature": {
|
||||
"created": "2025-06-12T18:44:16Z",
|
||||
"creator": "https://my-place.social/profile/vaartis#main-key",
|
||||
"nonce": "2d67847d4bd4b1b83a30d61eac6cdc7ad6b980df06a8b9b97217e1d8f7b6cf20",
|
||||
"signatureValue": "LnoRMZuQGDvTICkShGBq28ynaj2lF1bViJFGS6n4gKn3IbxPWATHxao43gxWRc+HCTrHNg7quzgaW4+PYM7UVUz3jO+bjNKsN845nijOVdyFrPOXbuaij3KQh2OoHhFJWoV/ZQQTFF0kRK1qT4BwG+P8NqOOKAMv+Cw7ruQH+f2w7uDgcNIbCD1gLcwb6cw7WVe5qu8yMkKqp2kBdqW3RCsI85RmmFgwehDgH5nrX7ER1qbeLWrqy7echwD9/fO3rqAu13xDNyiGZHDT7JB3RUt0AyMm0XCfjbwSQ0n+MkYXgE4asvFz81+iiPCLt+6gePWAFc5odF1FxdySBpSuUOs4p92NzP9OhQ0c0qrqrzYI7aYklY7oMfxjkva+X+0bm3up+2IRJdnZa/pXlmwdcqTpyMr1sgzaexMUNBp3dq7zA51eEaakLDX3i2onXJowfmze3+6XgPAFHYamR+pRNtuEoY4uyYEK3fj5GgwJ4RtFJMYVoEs/Q8h3OgYRcK1FE9UlDjSqbQ7QIRn2Ib4wjgmkeM0vrHIwh/1CtqA/M/6WuYFzCaJBc8O9ykpK9ZMbw64ToQXKf2SqhZsDoyTWRWTO1PXOk1XCAAElUh8/WCyeghvgqLXn0LHov4lmBsHA5iMUcLqBKD3GJIHd+ExrOFxMZs4mBLLGyz0p5joJ3NY=",
|
||||
"type": "RsaSignature2017"
|
||||
},
|
||||
"to": [
|
||||
"https://pl.kotobank.ch/users/vaartis",
|
||||
"https://mitra.social/users/silverpill",
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"type": "Undo"
|
||||
}
|
||||
56
test/fixtures/friendica-dislike.json
vendored
Normal file
56
test/fixtures/friendica-dislike.json
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{
|
||||
"Hashtag": "as:Hashtag",
|
||||
"PropertyValue": "schema:PropertyValue",
|
||||
"conversation": "ostatus:conversation",
|
||||
"dfrn": "http://purl.org/macgirvin/dfrn/1.0/",
|
||||
"diaspora": "https://diasporafoundation.org/ns/",
|
||||
"directMessage": "litepub:directMessage",
|
||||
"discoverable": "toot:discoverable",
|
||||
"featured": {
|
||||
"@id": "toot:featured",
|
||||
"@type": "@id"
|
||||
},
|
||||
"litepub": "http://litepub.social/ns#",
|
||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||
"ostatus": "http://ostatus.org#",
|
||||
"quoteUrl": "as:quoteUrl",
|
||||
"schema": "http://schema.org#",
|
||||
"sensitive": "as:sensitive",
|
||||
"toot": "http://joinmastodon.org/ns#",
|
||||
"value": "schema:value",
|
||||
"vcard": "http://www.w3.org/2006/vcard/ns#"
|
||||
}
|
||||
],
|
||||
"actor": "https://my-place.social/profile/vaartis",
|
||||
"cc": [
|
||||
"https://my-place.social/followers/vaartis"
|
||||
],
|
||||
"diaspora:guid": "e599373b-1968-4b20-cd24-80d340160302",
|
||||
"diaspora:like": "{\"author\":\"vaartis@my-place.social\",\"guid\":\"e599373b-1968-4b20-cd24-80d340160302\",\"parent_guid\":\"cd36feba-c31f3ed3fd5c064a-17c31593\",\"parent_type\":\"Post\",\"positive\":\"false\",\"author_signature\":\"xR2zLJNfc9Nhx1n8LLMWM1kde12my4cqamIsrH\\/UntKzuDwO4DuHBL0fkFhgC\\/dylxm4HqsHD45MQbtwQCVGq6WhC96TrbMuYEK61HIO23dTr3m+qJVtfdH4wyhUNHgiiYPhZpkLDfnR1JiRWmFTlmZC8q8JEkOB5IQsrWia2eOR6IsqDcdKO\\/Urgns9\\/BdQi8KnchBKSEFc1iUtcOEruvhunKGyW5zI\\/Rltfdz3xGH8tlw+YlMXeWXPnqgOJ9GzNA0lwG4U421L6yylYagW7oxIznnBLB4bO46vYZbgXZV1hiI9ZyveHOinLMY1QkmTj5CNvnx3\\/VJwLovd0v+0Nr2vu\\/3ftbpBXc6L1bsNjlRqtsfwJlcgl+tH1DC4W8tKf+Y3tdtzVw0CHXCuacxHLyq5wZd\\/5YfYR9SJQ\\/jInU4PHA5+hIE3PGqNUp5QfFE0umq56H7MQKsIPgM5mMV4fPAA8OpltuMVDvQYUxalrnvoTf00k90x1wCTK71\\/jQGh7r7PmGvSdfPr+65eVTjITD8\\/lTGIb8850v1fl3\\/i2R8Dv17jQIRyX5o9MXPSO6jHo4Swma5RzPA\\/0bRj6qRTyPkM1L9qEIr+2H2I7KKhT2ZE5GhAU7yI9A3VLBWzpTrUPMGbfpd1OjVTEqXAdMjpLDYI3Mh5zQ58p8xCzt+W+t0=\"}",
|
||||
"id": "https://my-place.social/objects/e599373b-1368-4b20-cd24-837166957182",
|
||||
"instrument": {
|
||||
"id": "https://my-place.social/friendica",
|
||||
"name": "Friendica 'Interrupted Fern' 2024.12-1576",
|
||||
"type": "Application",
|
||||
"url": "https://my-place.social"
|
||||
},
|
||||
"object": "https://pl.kotobank.ch/objects/301bce65-8a1b-4c49-a65c-fe2ce861a213",
|
||||
"published": "2025-06-12T18:47:41Z",
|
||||
"signature": {
|
||||
"created": "2025-06-12T18:47:42Z",
|
||||
"creator": "https://my-place.social/profile/vaartis#main-key",
|
||||
"nonce": "84e496f80b09d7a299c5cc89e8cadd13abf621b3a0a321684fa74278b68a6dd8",
|
||||
"signatureValue": "qe2WxY+j7daIYLRadCctgal6A1s9XgoiMfM/8KjJm15w0sSizYYqruyQ5gS44e+cj5GHc9v5gP2ieod5v7eHAPzlcDI4bfkcyHVapAXTqU67ZebW+v6Q+21IMDgqrkYCv5TbV7LTxltW59dlqovpHE4TEe/M7xLKWJ3vVchRUcWqH9kDmak0nacoqYVAb5E9jYnQhUWPTCfPV82qQpeWQPOZ4iIvPw6rDkSSY5jL6bCogBZblHGpUjXfe/FPlacaCWiTQdoga3yOBXB1RYPw9nh5FI5Xkv/oi+52WmJrECinlD6AL8/BpiYvKz236zy7p/TR4BXlCx9RR/msjOnSabkQ4kmYFrRr80UDCGF+CdkdzLl8K9rSE3PbF1+nEqD7X0GOWn/DdtixqXJw6IR4bh32YW2SlcrSRBvI1p82Mv68BeqRaYqL6FAhKFwLhX5JpXngZ3k0g7rWWxc498voPWnFZDyCTRNxO9VIIUavDDEQ0BdFk6WDb8zx9tsAg8JoK57eVDcFly7tfVQffYiHpve06d8ag1DtzipqguRsURmuqpGNMq28XBTxwtrP2LnXXHKxoYN/YQ9cDnCKclbx7/uKmOVMLkLZlM0wAVoZpm5z2fG4voKqFiGZ1PoiFY2sN4URMArJtygV3PlTX4ASAQrak0ksvEo9msrBUD0Su9c=",
|
||||
"type": "RsaSignature2017"
|
||||
},
|
||||
"to": [
|
||||
"https://pl.kotobank.ch/users/vaartis",
|
||||
"https://mitra.social/users/silverpill",
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"type": "Dislike"
|
||||
}
|
||||
151
test/fixtures/fulmo.html
vendored
Normal file
151
test/fixtures/fulmo.html
vendored
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang='eo'>
|
||||
<head>
|
||||
<meta charset='utf-8'/>
|
||||
<meta name='author' content='Tirifto'/>
|
||||
<meta name='generator' content='Pageling'/>
|
||||
<meta name='viewport' content='width=device-width,
|
||||
height=device-height,
|
||||
initial-scale=1.0'/>
|
||||
<link rel='stylesheet' type='text/css' href='/r/stiloj/tiriftejo.css'/>
|
||||
<link rel='alternate' type='application/atom+xml' href='/eo/novajhoj.atom'/>
|
||||
<link rel='icon' size='16x16' type='image/vnd.microsoft.icon' href='/favicon.ico'/>
|
||||
<link rel='icon' size='128x128' type='image/png' href='/icon.png'/>
|
||||
<link rel='alternate' hreflang='eo' href='https://tirifto.xwx.moe/eo/rakontoj/fulmo.html'/>
|
||||
<title>Fulmo</title>
|
||||
<meta property='og:title' content='Fulmo'/>
|
||||
<meta property='og:type' content='website'/>
|
||||
<meta property='og:url' content='https://tirifto.xwx.moe/eo/rakontoj/fulmo.html'/>
|
||||
<meta property='og:site_name' content='Tiriftejo'/>
|
||||
<meta property='og:locale' content='eo'/>
|
||||
<meta property='og:description' content='Pri feoj, kiuj devis ordigi falintan arbon.'/>
|
||||
<meta property='og:image' content='https://tirifto.xwx.moe/r/ilustrajhoj/pinglordigado.png'/>
|
||||
<meta property='og:image:alt' content='Meze de arbaro kuŝas falinta trunko, sen pingloj kaj kun branĉoj derompitaj. Post ĝi videblas du feoj: florofeo maldekstre kaj nubofeo dekstre. La florofeo iom kaŝas sin post la trunko. La nubofeo staras kaj tenas amason da pigloj. Ili iom rigardas al si.'/>
|
||||
<meta property='og:image:height' content='630'/>
|
||||
<meta property='og:image:width' content='1200'/>
|
||||
<meta property='og:image' content='https://tirifto.xwx.moe/r/opengraph/eo.png'/>
|
||||
<meta property='og:image:alt' content='La tirifta okulo ĉirkaŭita de ornamaj steloj kaj la teksto: »Tiriftejo. Esperanto.«'/>
|
||||
<meta property='og:image:height' content='630'/>
|
||||
<meta property='og:image:width' content='1200'/>
|
||||
</head>
|
||||
<body>
|
||||
<header id='website-header'>
|
||||
<nav id='website-navigation'>
|
||||
<input type='checkbox' id='website-navigation-toggle'
|
||||
aria-description='Montri ligilojn al ĉefaj paĝoj de la retejo.'/>
|
||||
<label for='website-navigation-toggle'>Paĝoj</label>
|
||||
<a href='/eo/verkoj.html'>Verkoj</a>
|
||||
<a href='/eo/novajhoj.html'>Novaĵoj</a>
|
||||
<a href='/eo/donacoj.html'>Donacoj</a>
|
||||
<a href='/eo/prio.html'>Prio</a>
|
||||
<a href='/eo/amikoj.html'>Amikoj</a>
|
||||
<a href='/eo/kontakto.html'>Kontakto</a>
|
||||
|
||||
</nav>
|
||||
<span id='eye' role='img' aria-label=''></span>
|
||||
<nav id='language-switcher'
|
||||
aria-roledescription='lingvo-ŝanĝilo'>
|
||||
<input type='checkbox' id='language-switcher-toggle'
|
||||
aria-description='Montri ligilojn al tradukoj de tiu ĉi paĝo.'/>
|
||||
<label for='language-switcher-toggle'>Lingvoj</label>
|
||||
<a href='fulmo.html' lang='eo' hreflang='eo'><img aria-hidden='true' alt='' src='/r/flagoj/eo.png'/>Esperanto</a>
|
||||
</nav>
|
||||
</header>
|
||||
<div class='bodier'>
|
||||
<nav id='work-links'>
|
||||
<a href='.'>Ceteraj rakontoj</a>
|
||||
<a href='../bildosignoj'>Bildosignoj</a>
|
||||
<a href='../eseoj'>Eseoj</a>
|
||||
<a href='../ludoj'>Ludoj</a>
|
||||
<a href='../poemoj'>Poemoj</a>
|
||||
<a href='../vortaroj'>Vortaroj</a>
|
||||
</nav>
|
||||
<main>
|
||||
<article>
|
||||
<header>
|
||||
<h1>Fulmo</h1>
|
||||
<p>Skribis Tirifto</p>
|
||||
<time datetime='2025-01-31'>2025-01-31</time>
|
||||
</header>
|
||||
<p>»Kial ĉiam mi? Tio ne justas! Oni kulpigas min, sed ja ne mi kulpas!« La nubofeo lamentis, dum ĝi ordigis restaĵojn de falinta arbo. Plejparto el la pingloj estis brulintaj, kaj el la trunko ankoraŭ leviĝis fumo.</p>
|
||||
<p>Subite aŭdeblis ekstraj kraketoj deapude. Ĝi rigardis flanken, kaj vidis iun kaŭri apud la arbo, derompi branĉetojn, kaj orde ilin amasigi. Ŝajnis, ke ekde sia rimarkiĝo, la nekonatulo laŭeble kuntiriĝis, kaj strebis labori kiel eble plej silente.</p>
|
||||
<p>»Saluton…?« La nubofeo stariĝis, alporolante la eston. Tiu kvazaŭ frostiĝis, sed timeme ankaŭ stariĝis.</p>
|
||||
<p>»S- Saluton…« Ĝi respondis sen kuraĝo rigardi ĝiadirekten. Nun stare, videblis ke ĝi estas verdanta florofeo.</p>
|
||||
<p>»… kion vi faras tie ĉi?« La nubofeo demandis.</p>
|
||||
<p>»Nu… tiel kaj tiel… mi ordigas.«</p>
|
||||
<p>»Ho. Mi ricevis taskon ordigi ĉi tie… se vi povas atendi, vi ne bezonas peni!«</p>
|
||||
<p>»N- Nu… mi tamen volus…« Parolis la florofeo, plu deturnante la kapon.</p>
|
||||
<p>»Nu… bone, se vi tion deziras… dankon!« La nubofeo dankis, kaj returniĝis al sia laboro.</p>
|
||||
<p>Fojfoje ĝi scivole rigardis al sia nova kunlaboranto, kaj fojfoje renkontis similan rigardon de ĝia flanko, kiuokaze ambaŭ rigardoj rapide revenis al la ordigataj pingloj kaj branĉetoj. »(Kial tiom volonte helpi min?)« Pensis al si la nubofeo. »(Ĉu ĝi simple tiom bonkoras? Ĝi ja tre bele floras; eble ankaŭ ĝia koro tiel same belas…)« Kaj vere, ĝiaj surfloroj grandanime malfermis siajn belkolorajn folietojn, kaj bonodoris al mondo.</p>
|
||||
<figure>
|
||||
<picture>
|
||||
<source srcset='/r/ilustrajhoj/pinglordigado.jxl' type='image/jxl'/>
|
||||
<img src='/r/ilustrajhoj/pinglordigado.png' alt='Meze de arbaro kuŝas falinta trunko, sen pingloj kaj kun branĉoj derompitaj. Post ĝi videblas du feoj: florofeo maldekstre kaj nubofeo dekstre. La florofeo iom kaŝas sin post la trunko. La nubofeo staras kaj tenas amason da pigloj. Ili iom rigardas al si.'/>
|
||||
</picture>
|
||||
<figcaption>
|
||||
Pinglordigado
|
||||
<details>
|
||||
<summary>© <time datetime='2025'>2025</time> Tirifto</summary>
|
||||
<a href='https://artlibre.org/'><img src='/r/permesiloj/lal.svg' class='stamp licence' alt='Emblemo: Permesilo de arto libera'/></a>
|
||||
</details>
|
||||
</figcaption>
|
||||
</figure>
|
||||
<p>Post iom da tempo, ĉiu feo tralaboris ĝis la trunkomezo, kaj proksimiĝis al la alia feo. Kaj tiam ekpezis sur ili devosento rompi la silenton.</p>
|
||||
<p>»… kia bela vetero, ĉu ne?« Diris la nubofeo, tuj rimarkonte, ke mallumiĝas, kaj la ĉielo restas kovrita de nuboj.</p>
|
||||
<p>»Jes ja! Tre nube. Mi ŝatas nubojn!« Respondis la alia entuziasme, sed tuj haltetis kaj deturnis la kapon. Ambaŭ feoj daŭrigis laboron silente, kaj plu proksimiĝis, ĝis tiu preskaŭ estis finita.</p>
|
||||
<p>»H… H… Ho ne…!« Subite ekdiris la nubofeo urĝe.</p>
|
||||
<p>»Kio okazas?!«</p>
|
||||
<p>»T… Tern…!«</p>
|
||||
<p>»Jen! Tenu!« La florofeo etendis manon kun granda folio. La nubofeo ĝin prenis, kaj tien ternis. Aperis ekfulmo, kaj la cindriĝinta folio disfalis.</p>
|
||||
<p>»Pardonu… mi ne volis…« Bedaŭris la nubofeo. »Mi ne scias, kial tio ĉiam okazas! Tiom plaĉas al mi promeni tere, sed ĉiuj diras, ke mi maldevus, ĉar ial ĝi ĉiam finiĝas tiel ĉi.« Ĝi montris al la arbo. »Eble ili pravas…«</p>
|
||||
<p>»Nu…« diris la florofeo bedaŭre, kaj etendis la manon.</p>
|
||||
<p>»H… H… Ne ree…!«</p>
|
||||
<p>Ekfulmis. Alia ĵus metita folio cindriĝis en la manoj de la florofeo, time ferminta la okulojn.</p>
|
||||
<p>»Dankegon… mi tre ŝatas vian helpon! Kaj mi ne… ne…«</p>
|
||||
<p>Metiĝis. Ekfulmis. Cindriĝis.</p>
|
||||
<p>»Io tre iritas mian nazon!« Plendis la nubofeo. Poste ĝi rimarkis la florpolvon, kiu disŝutiĝis el la florofeo en la tutan ĉirkaŭaĵon, kaj eĉ tuj antaŭ la nubofeon.</p>
|
||||
<p>»N- Nu…« Diris la florofeo, honte rigardanta la teron. »… pardonu.«</p>
|
||||
<footer>
|
||||
<noscript><p>Ĉu vi ŝatas la verkon? <a href='/eo/donacoj.html'>Subtenu min</a> aŭ kopiu adreson de la verko por diskonigi ĝin!</p></noscript>
|
||||
<script id='underbuttons'>
|
||||
/* @license magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt CC0-1.0 */
|
||||
document.getElementById('underbuttons').outerHTML = "<p><a href='/eo/donacoj.html' class='button' target='_blank'>Subtenu min</a> <button onclick='navigator.clipboard.writeText(window.location.href.split(\"\#\")[0]).then(() => window.alert(\"Ligilo al ĉi tiu verko estas kopiita. Sendu ĝin al iu por diskonigi la verkon! 🐱\"))'>Diskonigu la verkon</button></p>"
|
||||
/* @license-end */
|
||||
</script>
|
||||
<details class='history'>
|
||||
<summary>Historio</summary>
|
||||
<dl>
|
||||
<dt><time datetime='2025-01-31'>2025-01-31</time></dt>
|
||||
<dd>Unua publikigo.</dd>
|
||||
</dl>
|
||||
</details>
|
||||
<details class='licence' open='details'>
|
||||
<summary>Permesilo</summary>
|
||||
<p>Ĉi tiun verkon vi rajtas libere kopii, disdoni, kaj ŝanĝi, laŭ kondiĉoj de la <a href='https://artlibre.org/'>Permesilo de arto libera</a>. (Resume: Vi devas mencii la aŭtoron kaj doni ligilon al la verko. Se vi ŝanĝas la verkon, vi devas laŭeble noti la faritajn ŝanĝojn, ilian daton, kaj eldoni ilin sub la sama permesilo.)</p>
|
||||
<a href='https://artlibre.org/'><img src='/r/permesiloj/lal.svg' class='stamp licence' alt='Emblemo: Permesilo de arto libera'/></a>
|
||||
</details>
|
||||
</footer>
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
<footer id='website-footer'>
|
||||
<div class='stamps'>
|
||||
<a href='https://gnu.org/'>
|
||||
<img class='stamp' src='/r/retetikedoj/gnu.png' lang='en' alt='GNU'/></a>
|
||||
<img class='stamp' src='/r/retetikedoj/ihhtus.png' lang='el' alt='ΙΧΘΥΣ'/>
|
||||
<img class='stamp' src='/r/retetikedoj/be-kind.apng' lang='en' alt='Be kind.'/>
|
||||
<img class='stamp' src='/r/retetikedoj/kulturo-libera.png' lang='eo' alt='Kulturo libera.'/>
|
||||
<img class='stamp' src='/r/retetikedoj/discord.png' lang='en' alt='Say ‘no’ to Discord.'/>
|
||||
<a href='https://xwx.moe/'>
|
||||
<img class='stamp' src='/r/retetikedoj/xwx-moe.png' alt='xwx.moe'/></a>
|
||||
<a href='https://mojeek.co.uk' hreflang='en'>
|
||||
<img class='stamp' src='/r/retetikedoj/mojeek.png' lang='en' alt='Mojeek'/></a>
|
||||
<a href='https://raku.org/' hreflang='en'>
|
||||
<img class='stamp' src='/r/retetikedoj/raku.png' alt='Raku'/></a>
|
||||
<picture>
|
||||
<source srcset='/r/retetikedoj/jxl.jxl' type='image/jxl'/>
|
||||
<img class='stamp' src='/r/retetikedoj/jxl.png' alt='JPEG XL'/></picture>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
90
test/fixtures/mastodon-update-with-likes.json
vendored
Normal file
90
test/fixtures/mastodon-update-with-likes.json
vendored
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
{
|
||||
"atomUri": "ostatus:atomUri",
|
||||
"conversation": "ostatus:conversation",
|
||||
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
|
||||
"ostatus": "http://ostatus.org#",
|
||||
"sensitive": "as:sensitive",
|
||||
"toot": "http://joinmastodon.org/ns#",
|
||||
"votersCount": "toot:votersCount"
|
||||
},
|
||||
"https://w3id.org/security/v1"
|
||||
],
|
||||
"actor": "https://pol.social/users/mkljczk",
|
||||
"cc": ["https://www.w3.org/ns/activitystreams#Public",
|
||||
"https://pol.social/users/aemstuz", "https://gts.mkljczk.pl/users/mkljczk",
|
||||
"https://pl.fediverse.pl/users/mkljczk",
|
||||
"https://fedi.kutno.pl/users/mkljczk"],
|
||||
"id": "https://pol.social/users/mkljczk/statuses/113907871635572263#updates/1738096776",
|
||||
"object": {
|
||||
"atomUri": "https://pol.social/users/mkljczk/statuses/113907871635572263",
|
||||
"attachment": [],
|
||||
"attributedTo": "https://pol.social/users/mkljczk",
|
||||
"cc": ["https://www.w3.org/ns/activitystreams#Public",
|
||||
"https://pol.social/users/aemstuz", "https://gts.mkljczk.pl/users/mkljczk",
|
||||
"https://pl.fediverse.pl/users/mkljczk",
|
||||
"https://fedi.kutno.pl/users/mkljczk"],
|
||||
"content": "<p>test</p>",
|
||||
"contentMap": {
|
||||
"pl": "<p>test</p>"
|
||||
},
|
||||
"conversation": "https://fedi.kutno.pl/contexts/43c14c70-d3fb-42b4-a36d-4eacfab9695a",
|
||||
"id": "https://pol.social/users/mkljczk/statuses/113907871635572263",
|
||||
"inReplyTo": "https://pol.social/users/aemstuz/statuses/113907854282654767",
|
||||
"inReplyToAtomUri": "https://pol.social/users/aemstuz/statuses/113907854282654767",
|
||||
"likes": {
|
||||
"id": "https://pol.social/users/mkljczk/statuses/113907871635572263/likes",
|
||||
"totalItems": 1,
|
||||
"type": "Collection"
|
||||
},
|
||||
"published": "2025-01-28T20:29:45Z",
|
||||
"replies": {
|
||||
"first": {
|
||||
"items": [],
|
||||
"next": "https://pol.social/users/mkljczk/statuses/113907871635572263/replies?only_other_accounts=true&page=true",
|
||||
"partOf": "https://pol.social/users/mkljczk/statuses/113907871635572263/replies",
|
||||
"type": "CollectionPage"
|
||||
},
|
||||
"id": "https://pol.social/users/mkljczk/statuses/113907871635572263/replies",
|
||||
"type": "Collection"
|
||||
},
|
||||
"sensitive": false,
|
||||
"shares": {
|
||||
"id": "https://pol.social/users/mkljczk/statuses/113907871635572263/shares",
|
||||
"totalItems": 0,
|
||||
"type": "Collection"
|
||||
},
|
||||
"summary": null,
|
||||
"tag": [
|
||||
{
|
||||
"href": "https://pol.social/users/aemstuz",
|
||||
"name": "@aemstuz",
|
||||
"type": "Mention"
|
||||
},
|
||||
{
|
||||
"href": "https://gts.mkljczk.pl/users/mkljczk",
|
||||
"name": "@mkljczk@gts.mkljczk.pl",
|
||||
"type": "Mention"
|
||||
},
|
||||
{
|
||||
"href": "https://pl.fediverse.pl/users/mkljczk",
|
||||
"name": "@mkljczk@fediverse.pl",
|
||||
"type": "Mention"
|
||||
},
|
||||
{
|
||||
"href": "https://fedi.kutno.pl/users/mkljczk",
|
||||
"name": "@mkljczk@fedi.kutno.pl",
|
||||
"type": "Mention"
|
||||
}
|
||||
],
|
||||
"to": ["https://pol.social/users/mkljczk/followers"],
|
||||
"type": "Note",
|
||||
"updated": "2025-01-28T20:39:36Z",
|
||||
"url": "https://pol.social/@mkljczk/113907871635572263"
|
||||
},
|
||||
"published": "2025-01-28T20:39:36Z",
|
||||
"to": ["https://pol.social/users/mkljczk/followers"],
|
||||
"type": "Update"
|
||||
}
|
||||
54
test/fixtures/misskey-custom-emoji-like.json
vendored
Normal file
54
test/fixtures/misskey-custom-emoji-like.json
vendored
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{
|
||||
"Emoji": "toot:Emoji",
|
||||
"Hashtag": "as:Hashtag",
|
||||
"PropertyValue": "schema:PropertyValue",
|
||||
"_misskey_content": "misskey:_misskey_content",
|
||||
"_misskey_quote": "misskey:_misskey_quote",
|
||||
"_misskey_reaction": "misskey:_misskey_reaction",
|
||||
"_misskey_summary": "misskey:_misskey_summary",
|
||||
"_misskey_votes": "misskey:_misskey_votes",
|
||||
"backgroundUrl": "sharkey:backgroundUrl",
|
||||
"discoverable": "toot:discoverable",
|
||||
"featured": "toot:featured",
|
||||
"fedibird": "http://fedibird.com/ns#",
|
||||
"firefish": "https://joinfirefish.org/ns#",
|
||||
"isCat": "misskey:isCat",
|
||||
"listenbrainz": "sharkey:listenbrainz",
|
||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||
"misskey": "https://misskey-hub.net/ns#",
|
||||
"quoteUri": "fedibird:quoteUri",
|
||||
"quoteUrl": "as:quoteUrl",
|
||||
"schema": "http://schema.org#",
|
||||
"sensitive": "as:sensitive",
|
||||
"sharkey": "https://joinsharkey.org/ns#",
|
||||
"speakAsCat": "firefish:speakAsCat",
|
||||
"toot": "http://joinmastodon.org/ns#",
|
||||
"value": "schema:value",
|
||||
"vcard": "http://www.w3.org/2006/vcard/ns#"
|
||||
}
|
||||
],
|
||||
"_misskey_reaction": ":blobwtfnotlikethis:",
|
||||
"actor": "https://mai.waifuism.life/users/9otxaeemjqy70001",
|
||||
"content": ":blobwtfnotlikethis:",
|
||||
"id": "https://mai.waifuism.life/likes/9q2xifhrdnb0001b",
|
||||
"object": "https://bungle.online/notes/9q2xi2sy4k",
|
||||
"tag": [
|
||||
{
|
||||
"icon": {
|
||||
"mediaType": "image/png",
|
||||
"type": "Image",
|
||||
"url": "https://mai.waifuism.life/files/1b0510f2-1fb4-43f5-a399-10053bbd8f0f"
|
||||
},
|
||||
"id": "https://mai.waifuism.life/emojis/blobwtfnotlikethis",
|
||||
"name": ":blobwtfnotlikethis:",
|
||||
"type": "Emoji",
|
||||
"updated": "2024-02-07T02:21:46.497Z"
|
||||
}
|
||||
],
|
||||
"type": "Like"
|
||||
}
|
||||
|
||||
46
test/fixtures/mitra-custom-emoji-like.json
vendored
Normal file
46
test/fixtures/mitra-custom-emoji-like.json
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
"https://w3id.org/security/data-integrity/v1",
|
||||
{
|
||||
"Emoji": "toot:Emoji",
|
||||
"Hashtag": "as:Hashtag",
|
||||
"sensitive": "as:sensitive",
|
||||
"toot": "http://joinmastodon.org/ns#"
|
||||
}
|
||||
],
|
||||
"actor": "https://mitra.social/users/silverpill",
|
||||
"cc": [],
|
||||
"content": ":ablobcatheartsqueeze:",
|
||||
"id": "https://mitra.social/activities/like/0195a89a-a3a0-ead4-3a1c-aa6311397cfd",
|
||||
"object": "https://framapiaf.org/users/peertube/statuses/114182703352270287",
|
||||
"proof": {
|
||||
"created": "2025-03-18T09:34:21.610678375Z",
|
||||
"cryptosuite": "eddsa-jcs-2022",
|
||||
"proofPurpose": "assertionMethod",
|
||||
"proofValue": "z5AvpwkXQGFpTneRVDNeF48Jo9qYG6PgrE5HaPPpQNdNyc31ULMN4Vxd4aFXELo4Rk5Y9hd9nDy254xP8v5uGGWp1",
|
||||
"type": "DataIntegrityProof",
|
||||
"verificationMethod": "https://mitra.social/users/silverpill#ed25519-key"
|
||||
},
|
||||
"tag": [
|
||||
{
|
||||
"attributedTo": "https://mitra.social/actor",
|
||||
"icon": {
|
||||
"mediaType": "image/png",
|
||||
"type": "Image",
|
||||
"url": "https://mitra.social/media/a08e153441b25e512ab1b2e8922f5d8cd928322c8b79958cd48297ac722a4117.png"
|
||||
},
|
||||
"id": "https://mitra.social/objects/emojis/ablobcatheartsqueeze",
|
||||
"name": ":ablobcatheartsqueeze:",
|
||||
"type": "Emoji",
|
||||
"updated": "1970-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"to": [
|
||||
"https://framapiaf.org/users/peertube",
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"type": "Like"
|
||||
}
|
||||
|
||||
93
test/fixtures/quote_post/mastodon_quote_post.json
vendored
Normal file
93
test/fixtures/quote_post/mastodon_quote_post.json
vendored
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
{
|
||||
"ostatus": "http://ostatus.org#",
|
||||
"atomUri": "ostatus:atomUri",
|
||||
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
|
||||
"conversation": "ostatus:conversation",
|
||||
"sensitive": "as:sensitive",
|
||||
"toot": "http://joinmastodon.org/ns#",
|
||||
"votersCount": "toot:votersCount",
|
||||
"quote": "https://w3id.org/fep/044f#quote",
|
||||
"quoteUri": "http://fedibird.com/ns#quoteUri",
|
||||
"_misskey_quote": "https://misskey-hub.net/ns#_misskey_quote",
|
||||
"quoteAuthorization": {
|
||||
"@id": "https://w3id.org/fep/044f#quoteAuthorization",
|
||||
"@type": "@id"
|
||||
},
|
||||
"gts": "https://gotosocial.org/ns#",
|
||||
"interactionPolicy": {
|
||||
"@id": "gts:interactionPolicy",
|
||||
"@type": "@id"
|
||||
},
|
||||
"canQuote": {
|
||||
"@id": "gts:canQuote",
|
||||
"@type": "@id"
|
||||
},
|
||||
"automaticApproval": {
|
||||
"@id": "gts:automaticApproval",
|
||||
"@type": "@id"
|
||||
},
|
||||
"manualApproval": {
|
||||
"@id": "gts:manualApproval",
|
||||
"@type": "@id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": "https://mastodon.social/users/gwynnion/statuses/115345489087257171",
|
||||
"type": "Note",
|
||||
"summary": null,
|
||||
"inReplyTo": null,
|
||||
"published": "2025-10-09T17:54:47Z",
|
||||
"url": "https://mastodon.social/@gwynnion/115345489087257171",
|
||||
"attributedTo": "https://mastodon.social/users/gwynnion",
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"cc": [
|
||||
"https://mastodon.social/users/gwynnion/followers"
|
||||
],
|
||||
"sensitive": false,
|
||||
"atomUri": "https://mastodon.social/users/gwynnion/statuses/115345489087257171",
|
||||
"inReplyToAtomUri": null,
|
||||
"conversation": "https://mastodon.social/contexts/109836797527169643-115345489087257171",
|
||||
"context": "https://mastodon.social/contexts/109836797527169643-115345489087257171",
|
||||
"content": "<p class=\"quote-inline\">RE: <a href=\"https://mastodon.social/@404mediaco/115344945575874225\" target=\"_blank\" rel=\"nofollow noopener\" translate=\"no\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">mastodon.social/@404mediaco/11</span><span class=\"invisible\">5344945575874225</span></a></p><p>Every age verification system is just a scheme for companies and hackers to steal your identity.</p>",
|
||||
"contentMap": {
|
||||
"en": "<p class=\"quote-inline\">RE: <a href=\"https://mastodon.social/@404mediaco/115344945575874225\" target=\"_blank\" rel=\"nofollow noopener\" translate=\"no\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">mastodon.social/@404mediaco/11</span><span class=\"invisible\">5344945575874225</span></a></p><p>Every age verification system is just a scheme for companies and hackers to steal your identity.</p>"
|
||||
},
|
||||
"quote": "https://mastodon.social/users/404mediaco/statuses/115344945575874225",
|
||||
"_misskey_quote": "https://mastodon.social/users/404mediaco/statuses/115344945575874225",
|
||||
"quoteUri": "https://mastodon.social/users/404mediaco/statuses/115344945575874225",
|
||||
"quoteAuthorization": "https://mastodon.social/users/404mediaco/quote_authorizations/115345489087269783",
|
||||
"interactionPolicy": {
|
||||
"canQuote": {
|
||||
"automaticApproval": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
]
|
||||
}
|
||||
},
|
||||
"attachment": [],
|
||||
"tag": [],
|
||||
"replies": {
|
||||
"id": "https://mastodon.social/users/gwynnion/statuses/115345489087257171/replies",
|
||||
"type": "Collection",
|
||||
"first": {
|
||||
"type": "CollectionPage",
|
||||
"next": "https://mastodon.social/users/gwynnion/statuses/115345489087257171/replies?only_other_accounts=true&page=true",
|
||||
"partOf": "https://mastodon.social/users/gwynnion/statuses/115345489087257171/replies",
|
||||
"items": []
|
||||
}
|
||||
},
|
||||
"likes": {
|
||||
"id": "https://mastodon.social/users/gwynnion/statuses/115345489087257171/likes",
|
||||
"type": "Collection",
|
||||
"totalItems": 26
|
||||
},
|
||||
"shares": {
|
||||
"id": "https://mastodon.social/users/gwynnion/statuses/115345489087257171/shares",
|
||||
"type": "Collection",
|
||||
"totalItems": 28
|
||||
}
|
||||
}
|
||||
109
test/fixtures/receiver_worker_signature_activity.json
vendored
Normal file
109
test/fixtures/receiver_worker_signature_activity.json
vendored
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{
|
||||
"claim": {
|
||||
"@id": "toot:claim",
|
||||
"@type": "@id"
|
||||
},
|
||||
"memorial": "toot:memorial",
|
||||
"atomUri": "ostatus:atomUri",
|
||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||
"blurhash": "toot:blurhash",
|
||||
"ostatus": "http://ostatus.org#",
|
||||
"discoverable": "toot:discoverable",
|
||||
"focalPoint": {
|
||||
"@container": "@list",
|
||||
"@id": "toot:focalPoint"
|
||||
},
|
||||
"votersCount": "toot:votersCount",
|
||||
"Hashtag": "as:Hashtag",
|
||||
"Emoji": "toot:Emoji",
|
||||
"alsoKnownAs": {
|
||||
"@id": "as:alsoKnownAs",
|
||||
"@type": "@id"
|
||||
},
|
||||
"sensitive": "as:sensitive",
|
||||
"movedTo": {
|
||||
"@id": "as:movedTo",
|
||||
"@type": "@id"
|
||||
},
|
||||
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
|
||||
"conversation": "ostatus:conversation",
|
||||
"Device": "toot:Device",
|
||||
"schema": "http://schema.org#",
|
||||
"toot": "http://joinmastodon.org/ns#",
|
||||
"cipherText": "toot:cipherText",
|
||||
"suspended": "toot:suspended",
|
||||
"messageType": "toot:messageType",
|
||||
"featuredTags": {
|
||||
"@id": "toot:featuredTags",
|
||||
"@type": "@id"
|
||||
},
|
||||
"Curve25519Key": "toot:Curve25519Key",
|
||||
"deviceId": "toot:deviceId",
|
||||
"Ed25519Signature": "toot:Ed25519Signature",
|
||||
"featured": {
|
||||
"@id": "toot:featured",
|
||||
"@type": "@id"
|
||||
},
|
||||
"devices": {
|
||||
"@id": "toot:devices",
|
||||
"@type": "@id"
|
||||
},
|
||||
"value": "schema:value",
|
||||
"PropertyValue": "schema:PropertyValue",
|
||||
"messageFranking": "toot:messageFranking",
|
||||
"publicKeyBase64": "toot:publicKeyBase64",
|
||||
"identityKey": {
|
||||
"@id": "toot:identityKey",
|
||||
"@type": "@id"
|
||||
},
|
||||
"Ed25519Key": "toot:Ed25519Key",
|
||||
"indexable": "toot:indexable",
|
||||
"EncryptedMessage": "toot:EncryptedMessage",
|
||||
"fingerprintKey": {
|
||||
"@id": "toot:fingerprintKey",
|
||||
"@type": "@id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"actor": "https://phpc.social/users/denniskoch",
|
||||
"cc": [],
|
||||
"id": "https://phpc.social/users/denniskoch/statuses/112847382711461301/activity",
|
||||
"inReplyTo": null,
|
||||
"inReplyToAtomUri": null,
|
||||
"object": {
|
||||
"atomUri": "https://phpc.social/users/denniskoch/statuses/112847382711461301",
|
||||
"attachment": [],
|
||||
"attributedTo": "https://phpc.social/users/denniskoch",
|
||||
"cc": [],
|
||||
"content": "<p><span class=\"h-card\" translate=\"no\"><a href=\"https://mastodon.social/@bastianallgeier\" class=\"u-url mention\">@<span>bastianallgeier</span></a></span> <span class=\"h-card\" translate=\"no\"><a href=\"https://chaos.social/@distantnative\" class=\"u-url mention\">@<span>distantnative</span></a></span> <span class=\"h-card\" translate=\"no\"><a href=\"https://fosstodon.org/@kev\" class=\"u-url mention\">@<span>kev</span></a></span> Another main argument: Discord is popular. Many people have an account, so you can just join an server quickly. Also you know the app and how to get around.</p>",
|
||||
"contentMap": {
|
||||
"en": "<p><span class=\"h-card\" translate=\"no\"><a href=\"https://mastodon.social/@bastianallgeier\" class=\"u-url mention\">@<span>bastianallgeier</span></a></span> <span class=\"h-card\" translate=\"no\"><a href=\"https://chaos.social/@distantnative\" class=\"u-url mention\">@<span>distantnative</span></a></span> <span class=\"h-card\" translate=\"no\"><a href=\"https://fosstodon.org/@kev\" class=\"u-url mention\">@<span>kev</span></a></span> Another main argument: Discord is popular. Many people have an account, so you can just join an server quickly. Also you know the app and how to get around.</p>"
|
||||
},
|
||||
"conversation": "tag:mastodon.social,2024-07-25:objectId=760068442:objectType=Conversation",
|
||||
"id": "https://phpc.social/users/denniskoch/statuses/112847382711461301",
|
||||
"published": "2024-07-25T13:33:29Z",
|
||||
"replies": null,
|
||||
"sensitive": false,
|
||||
"tag": [],
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"type": "Note",
|
||||
"url": "https://phpc.social/@denniskoch/112847382711461301"
|
||||
},
|
||||
"published": "2024-07-25T13:33:29Z",
|
||||
"signature": {
|
||||
"created": "2024-07-25T13:33:29Z",
|
||||
"creator": "https://phpc.social/users/denniskoch#main-key",
|
||||
"signatureValue": "slz9BKJzd2n1S44wdXGOU+bV/wsskdgAaUpwxj8R16mYOL8+DTpE6VnfSKoZGsBBJT8uG5gnVfVEz1YsTUYtymeUgLMh7cvd8VnJnZPS+oixbmBRVky/Myf91TEgQQE7G4vDmTdB4ii54hZrHcOOYYf5FKPNRSkMXboKA6LMqNtekhbI+JTUJYIB02WBBK6PUyo15f6B1RJ6HGWVgud9NE0y1EZXfrkqUt682p8/9D49ORf7AwjXUJibKic2RbPvhEBj70qUGfBm4vvgdWhSUn1IG46xh+U0+NrTSUED82j1ZVOeua/2k/igkGs8cSBkY35quXTkPz6gbqCCH66CuA==",
|
||||
"type": "RsaSignature2017"
|
||||
},
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"type": "Create"
|
||||
}
|
||||
90
test/fixtures/rich_media/instagram_longtext.html
vendored
Normal file
90
test/fixtures/rich_media/instagram_longtext.html
vendored
Normal file
File diff suppressed because one or more lines are too long
18
test/fixtures/server.pem
vendored
Normal file
18
test/fixtures/server.pem
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIICpDCCAYwCCQC0vCQAnSoGdzANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
|
||||
b2NhbGhvc3QwHhcNMjYwMTE2MTY1ODE5WhcNMzYwMTE0MTY1ODE5WjAUMRIwEAYD
|
||||
VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCq
|
||||
dZ4O2upZqwIo1eK5KrW1IIsjkfsFK8hE7Llh+4axcesiUKot0ib1CUhRSYiL1DLO
|
||||
CIYQOw8IKQDVSC4JWAX9SsnX4W8dwexMQuSQG7/IKX2auC1bNNySFvoqM6Gq3GL9
|
||||
MqBFonZGXDPZu8fmxsI/2p9+2GK13F+HXgoLlXSCoO3XELJaBmjv29tgxxWRxCiH
|
||||
m4u0briSxgUEx+CctpKPvGDmLaoIOIhjtuoG6OjkeWUOp6jDcteazO23VxPyF5cS
|
||||
NbRJgm8AckrTQ6wbWSnhyqF8rPEsIc0ZAlUdDEs5fL3sjugc566FvE+GOkZIEyDD
|
||||
tgWbc4Ne+Kp/nnt6oVxpAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADv+J1DTok8V
|
||||
MKVKo0hsRnHTeJQ2+EIgOspuYlEzez3PysOZH6diAQxO2lzuo9LKxP3hnmw17XO/
|
||||
P2oCzYyb9/P58VY/gr4UDIfuhgcE0cVfdsRhVId/I2FW6VP2f5q1TGbDUxSsVIlG
|
||||
6hufn1aLBu90LtEbDkHqbnD05yYPwdqzWg4TrOXbX+jBhQrXJJdB3W7KTgozjRQw
|
||||
F7+/2IyXoxXuxcwQBQlYhUbvGlsFqFpP/6cz2al5i5pNUkiNaSYwlRmuwa7zoTft
|
||||
tHf57dhfXIpXET2BaJM6DSjDOOG/QleRXkvkTI5J21q+Bo+XnOzo19p4cZKJpTFC
|
||||
SNgrftyNh3k=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
1
test/fixtures/tesla_mock/deepl-languages-list.json
vendored
Normal file
1
test/fixtures/tesla_mock/deepl-languages-list.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[{"language":"BG","name":"Bulgarian","supports_formality":false},{"language":"CS","name":"Czech","supports_formality":false},{"language":"DA","name":"Danish","supports_formality":false},{"language":"DE","name":"German","supports_formality":true},{"language":"EL","name":"Greek","supports_formality":false},{"language":"EN-GB","name":"English (British)","supports_formality":false},{"language":"EN-US","name":"English (American)","supports_formality":false},{"language":"ES","name":"Spanish","supports_formality":true},{"language":"ET","name":"Estonian","supports_formality":false},{"language":"FI","name":"Finnish","supports_formality":false},{"language":"FR","name":"French","supports_formality":true},{"language":"HU","name":"Hungarian","supports_formality":false},{"language":"ID","name":"Indonesian","supports_formality":false},{"language":"IT","name":"Italian","supports_formality":true},{"language":"JA","name":"Japanese","supports_formality":false},{"language":"LT","name":"Lithuanian","supports_formality":false},{"language":"LV","name":"Latvian","supports_formality":false},{"language":"NL","name":"Dutch","supports_formality":true},{"language":"PL","name":"Polish","supports_formality":true},{"language":"PT-BR","name":"Portuguese (Brazilian)","supports_formality":true},{"language":"PT-PT","name":"Portuguese (European)","supports_formality":true},{"language":"RO","name":"Romanian","supports_formality":false},{"language":"RU","name":"Russian","supports_formality":true},{"language":"SK","name":"Slovak","supports_formality":false},{"language":"SL","name":"Slovenian","supports_formality":false},{"language":"SV","name":"Swedish","supports_formality":false},{"language":"TR","name":"Turkish","supports_formality":false},{"language":"UK","name":"Ukrainian","supports_formality":false},{"language":"ZH","name":"Chinese (simplified)","supports_formality":false}]
|
||||
1
test/fixtures/tesla_mock/deepl-translation.json
vendored
Normal file
1
test/fixtures/tesla_mock/deepl-translation.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"translations":[{"detected_source_language":"PL","text":"REMOVE THE FOLLOWER!Paste this on your follower. If we get 70% of nk users...they will remove the follower!!!"}]}
|
||||
27
test/fixtures/unindexed_fk.sql
vendored
Normal file
27
test/fixtures/unindexed_fk.sql
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
-- Unindexed FK -- Missing indexes - For CI
|
||||
|
||||
WITH y AS (
|
||||
SELECT
|
||||
pg_catalog.format('%I', c1.relname) AS referencing_tbl,
|
||||
pg_catalog.quote_ident(a1.attname) AS referencing_column,
|
||||
(SELECT pg_get_expr(indpred, indrelid) FROM pg_catalog.pg_index WHERE indrelid = t.conrelid AND indkey[0] = t.conkey[1] AND indpred IS NOT NULL LIMIT 1) partial_statement
|
||||
FROM pg_catalog.pg_constraint t
|
||||
JOIN pg_catalog.pg_attribute a1 ON a1.attrelid = t.conrelid AND a1.attnum = t.conkey[1]
|
||||
JOIN pg_catalog.pg_class c1 ON c1.oid = t.conrelid
|
||||
JOIN pg_catalog.pg_namespace n1 ON n1.oid = c1.relnamespace
|
||||
JOIN pg_catalog.pg_class c2 ON c2.oid = t.confrelid
|
||||
JOIN pg_catalog.pg_namespace n2 ON n2.oid = c2.relnamespace
|
||||
JOIN pg_catalog.pg_attribute a2 ON a2.attrelid = t.confrelid AND a2.attnum = t.confkey[1]
|
||||
WHERE t.contype = 'f'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_catalog.pg_index i
|
||||
WHERE i.indrelid = t.conrelid
|
||||
AND i.indkey[0] = t.conkey[1]
|
||||
AND indpred IS NULL
|
||||
)
|
||||
)
|
||||
SELECT referencing_tbl || '.' || referencing_column as "column"
|
||||
FROM y
|
||||
WHERE (partial_statement IS NULL OR partial_statement <> ('(' || referencing_column || ' IS NOT NULL)'))
|
||||
ORDER BY 1;
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"directMessage": "litepub:directMessage"
|
||||
}
|
||||
],
|
||||
"id": "http://localhost:8080/followers/fuser3",
|
||||
"id": "https://remote.org/followers/fuser3",
|
||||
"type": "OrderedCollection",
|
||||
"totalItems": 296
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
"directMessage": "litepub:directMessage"
|
||||
}
|
||||
],
|
||||
"id": "http://localhost:8080/following/fuser3",
|
||||
"id": "https://remote.org/following/fuser3",
|
||||
"type": "OrderedCollection",
|
||||
"totalItems": 32
|
||||
}
|
||||
|
|
|
|||
41
test/fixtures/users_mock/href_as_array.json
vendored
Normal file
41
test/fixtures/users_mock/href_as_array.json
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"alsoKnownAs": [],
|
||||
"attachment": [],
|
||||
"capabilities": {},
|
||||
"discoverable": true,
|
||||
"endpoints": {},
|
||||
"featured": "https://queef.in/cute_cat/collections/featured",
|
||||
"followers": "https://queef.in/cute_cat/followers",
|
||||
"following": "https://queef.in/cute_cat/following",
|
||||
"icon": {
|
||||
"type": "Image",
|
||||
"url": [
|
||||
"https://queef.in/storage/profile.webp",
|
||||
"https://example.com/image"
|
||||
]
|
||||
},
|
||||
"id": "https://queef.in/cute_cat",
|
||||
"image": {
|
||||
"type": "Image",
|
||||
"url": [
|
||||
"https://queef.in/storage/banner.gif",
|
||||
"https://example.com/image"
|
||||
]
|
||||
},
|
||||
"inbox": "https://queef.in/cute_cat/inbox",
|
||||
"manuallyApprovesFollowers": false,
|
||||
"name": "cute_cat",
|
||||
"outbox": "https://queef.in/cute_cat/outbox",
|
||||
"preferredUsername": "cute_cat",
|
||||
"publicKey": {
|
||||
"id": "https://queef.in/cute_cat#main-key",
|
||||
"owner": "https://queef.in/cute_cat"
|
||||
},
|
||||
"published": "2025-08-18T01:16:10.000Z",
|
||||
"summary": "A cute cat",
|
||||
"tag": [],
|
||||
"type": "Person",
|
||||
"url": "https://queef.in/cute_cat",
|
||||
"vcard:bday": null,
|
||||
"webfinger": "acct:cute_cat@queef.in"
|
||||
}
|
||||
1
test/fixtures/users_mock/hubzilla-actor-alsoknownas-string.json
vendored
Normal file
1
test/fixtures/users_mock/hubzilla-actor-alsoknownas-string.json
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"id": "http://localhost:4001/users/masto_closed/followers",
|
||||
"id": "https://remote.org/users/masto_closed/followers",
|
||||
"type": "OrderedCollection",
|
||||
"totalItems": 437,
|
||||
"first": "http://localhost:4001/users/masto_closed/followers?page=1"
|
||||
"first": "https://remote.org/users/masto_closed/followers?page=1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:4001/users/masto_closed/followers?page=1","type":"OrderedCollectionPage","totalItems":437,"next":"http://localhost:4001/users/masto_closed/followers?page=2","partOf":"http://localhost:4001/users/masto_closed/followers","orderedItems":["https://testing.uguu.ltd/users/rin","https://patch.cx/users/rin","https://letsalllovela.in/users/xoxo","https://pleroma.site/users/crushv","https://aria.company/users/boris","https://kawen.space/users/crushv","https://freespeech.host/users/cvcvcv","https://pleroma.site/users/picpub","https://pixelfed.social/users/nosleep","https://boopsnoot.gq/users/5c1896d162f7d337f90492a3","https://pikachu.rocks/users/waifu","https://royal.crablettesare.life/users/crablettes"]}
|
||||
{"@context":"https://www.w3.org/ns/activitystreams","id":"https://remote.org/users/masto_closed/followers?page=1","type":"OrderedCollectionPage","totalItems":437,"next":"https://remote.org/users/masto_closed/followers?page=2","partOf":"https://remote.org/users/masto_closed/followers","orderedItems":["https://testing.uguu.ltd/users/rin","https://patch.cx/users/rin","https://letsalllovela.in/users/xoxo","https://pleroma.site/users/crushv","https://aria.company/users/boris","https://kawen.space/users/crushv","https://freespeech.host/users/cvcvcv","https://pleroma.site/users/picpub","https://pixelfed.social/users/nosleep","https://boopsnoot.gq/users/5c1896d162f7d337f90492a3","https://pikachu.rocks/users/waifu","https://royal.crablettesare.life/users/crablettes"]}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"id": "http://localhost:4001/users/masto_closed/following",
|
||||
"id": "https://remote.org/users/masto_closed/following",
|
||||
"type": "OrderedCollection",
|
||||
"totalItems": 152,
|
||||
"first": "http://localhost:4001/users/masto_closed/following?page=1"
|
||||
"first": "https://remote.org/users/masto_closed/following?page=1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:4001/users/masto_closed/following?page=1","type":"OrderedCollectionPage","totalItems":152,"next":"http://localhost:4001/users/masto_closed/following?page=2","partOf":"http://localhost:4001/users/masto_closed/following","orderedItems":["https://testing.uguu.ltd/users/rin","https://patch.cx/users/rin","https://letsalllovela.in/users/xoxo","https://pleroma.site/users/crushv","https://aria.company/users/boris","https://kawen.space/users/crushv","https://freespeech.host/users/cvcvcv","https://pleroma.site/users/picpub","https://pixelfed.social/users/nosleep","https://boopsnoot.gq/users/5c1896d162f7d337f90492a3","https://pikachu.rocks/users/waifu","https://royal.crablettesare.life/users/crablettes"]}
|
||||
{"@context":"https://www.w3.org/ns/activitystreams","id":"https://remote.org/users/masto_closed/following?page=1","type":"OrderedCollectionPage","totalItems":152,"next":"https://remote.org/users/masto_closed/following?page=2","partOf":"https://remote.org/users/masto_closed/following","orderedItems":["https://testing.uguu.ltd/users/rin","https://patch.cx/users/rin","https://letsalllovela.in/users/xoxo","https://pleroma.site/users/crushv","https://aria.company/users/boris","https://kawen.space/users/crushv","https://freespeech.host/users/cvcvcv","https://pleroma.site/users/picpub","https://pixelfed.social/users/nosleep","https://boopsnoot.gq/users/5c1896d162f7d337f90492a3","https://pikachu.rocks/users/waifu","https://royal.crablettesare.life/users/crablettes"]}
|
||||
|
|
|
|||
10
test/fixtures/users_mock/pleroma_followers.json
vendored
10
test/fixtures/users_mock/pleroma_followers.json
vendored
|
|
@ -1,18 +1,18 @@
|
|||
{
|
||||
"type": "OrderedCollection",
|
||||
"totalItems": 527,
|
||||
"id": "http://localhost:4001/users/fuser2/followers",
|
||||
"id": "https://remote.org/users/fuser2/followers",
|
||||
"first": {
|
||||
"type": "OrderedCollectionPage",
|
||||
"totalItems": 527,
|
||||
"partOf": "http://localhost:4001/users/fuser2/followers",
|
||||
"partOf": "https://remote.org/users/fuser2/followers",
|
||||
"orderedItems": [],
|
||||
"next": "http://localhost:4001/users/fuser2/followers?page=2",
|
||||
"id": "http://localhost:4001/users/fuser2/followers?page=1"
|
||||
"next": "https://remote.org/users/fuser2/followers?page=2",
|
||||
"id": "https://remote.org/users/fuser2/followers?page=1"
|
||||
},
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"http://localhost:4001/schemas/litepub-0.1.jsonld",
|
||||
"https://remote.org/schemas/litepub-0.1.jsonld",
|
||||
{
|
||||
"@language": "und"
|
||||
}
|
||||
|
|
|
|||
10
test/fixtures/users_mock/pleroma_following.json
vendored
10
test/fixtures/users_mock/pleroma_following.json
vendored
|
|
@ -1,18 +1,18 @@
|
|||
{
|
||||
"type": "OrderedCollection",
|
||||
"totalItems": 267,
|
||||
"id": "http://localhost:4001/users/fuser2/following",
|
||||
"id": "https://remote.org/users/fuser2/following",
|
||||
"first": {
|
||||
"type": "OrderedCollectionPage",
|
||||
"totalItems": 267,
|
||||
"partOf": "http://localhost:4001/users/fuser2/following",
|
||||
"partOf": "https://remote.org/users/fuser2/following",
|
||||
"orderedItems": [],
|
||||
"next": "http://localhost:4001/users/fuser2/following?page=2",
|
||||
"id": "http://localhost:4001/users/fuser2/following?page=1"
|
||||
"next": "https://remote.org/users/fuser2/following?page=2",
|
||||
"id": "https://remote.org/users/fuser2/following?page=1"
|
||||
},
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"http://localhost:4001/schemas/litepub-0.1.jsonld",
|
||||
"https://remote.org/schemas/litepub-0.1.jsonld",
|
||||
{
|
||||
"@language": "und"
|
||||
}
|
||||
|
|
|
|||
1
test/fixtures/warnings/otp_version/21.1
vendored
1
test/fixtures/warnings/otp_version/21.1
vendored
|
|
@ -1 +0,0 @@
|
|||
21.1
|
||||
1
test/fixtures/warnings/otp_version/22.1
vendored
1
test/fixtures/warnings/otp_version/22.1
vendored
|
|
@ -1 +0,0 @@
|
|||
22.1
|
||||
1
test/fixtures/warnings/otp_version/22.4
vendored
1
test/fixtures/warnings/otp_version/22.4
vendored
|
|
@ -1 +0,0 @@
|
|||
22.4
|
||||
1
test/fixtures/warnings/otp_version/23.0
vendored
1
test/fixtures/warnings/otp_version/23.0
vendored
|
|
@ -1 +0,0 @@
|
|||
23.0
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Mix.PleromaTest do
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnit.Case, async: false
|
||||
import Mix.Pleroma
|
||||
|
||||
setup_all do
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Mix.Tasks.Pleroma.AppTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
use Pleroma.DataCase, async: false
|
||||
|
||||
setup_all do
|
||||
Mix.shell(Mix.Shell.Process)
|
||||
|
|
@ -42,9 +42,10 @@ defmodule Mix.Tasks.Pleroma.AppTest do
|
|||
|
||||
test "with errors" do
|
||||
Mix.Tasks.Pleroma.App.run(["create"])
|
||||
{:mix_shell, :error, ["Creating failed:"]}
|
||||
{:mix_shell, :error, ["name: can't be blank"]}
|
||||
{:mix_shell, :error, ["redirect_uris: can't be blank"]}
|
||||
|
||||
assert_receive {:mix_shell, :error, ["Creating failed:"]}
|
||||
assert_receive {:mix_shell, :error, ["name: can't be blank"]}
|
||||
assert_receive {:mix_shell, :error, ["redirect_uris: can't be blank"]}
|
||||
end
|
||||
|
||||
defp assert_app(name, redirect, scopes) do
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
|
|||
clear_config(:configurable_from_database, true)
|
||||
end
|
||||
|
||||
@tag capture_log: true
|
||||
test "config migration refused when deprecated settings are found" do
|
||||
clear_config([:media_proxy, :whitelist], ["domain_without_scheme.com"])
|
||||
assert config_records() == []
|
||||
|
|
@ -145,7 +144,13 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
|
|||
quarantined_instances: [],
|
||||
managed_config: true,
|
||||
static_dir: "instance/static/",
|
||||
allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"],
|
||||
allowed_post_formats: [
|
||||
"text/plain",
|
||||
"text/html",
|
||||
"text/markdown",
|
||||
"text/bbcode",
|
||||
"text/x.misskeymarkdown"
|
||||
],
|
||||
autofollowed_nicknames: [],
|
||||
max_pinned_statuses: 1,
|
||||
attachment_links: false,
|
||||
|
|
@ -330,5 +335,73 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
|
|||
|
||||
assert config_records() == []
|
||||
end
|
||||
|
||||
test "filters non-whitelisted settings" do
|
||||
clear_config(:database_config_whitelist, [
|
||||
{:pleroma},
|
||||
{:web_push_encryption, :vapid_details}
|
||||
])
|
||||
|
||||
insert_config_record(:web_push_encryption, :non_whitelisted_key, a: 1)
|
||||
insert_config_record(:web_push_encryption, :vapid_details, b: 1)
|
||||
|
||||
MixTask.run(["filter_whitelisted", "--force"])
|
||||
|
||||
assert [
|
||||
%ConfigDB{group: :pleroma, key: :instance},
|
||||
%ConfigDB{group: :pleroma, key: Pleroma.Captcha},
|
||||
%ConfigDB{group: :web_push_encryption, key: :vapid_details}
|
||||
] = config_records()
|
||||
end
|
||||
|
||||
test "filter_whitelisted doesn't crash when whitelist is unset" do
|
||||
clear_config(:database_config_whitelist, nil)
|
||||
|
||||
existing = config_records()
|
||||
MixTask.run(["filter_whitelisted", "--force"])
|
||||
assert config_records() == existing
|
||||
end
|
||||
|
||||
test "filter_whitelisted doesn't crash when whitelist is disabled" do
|
||||
clear_config(:database_config_whitelist, false)
|
||||
|
||||
existing = config_records()
|
||||
MixTask.run(["filter_whitelisted", "--force"])
|
||||
assert config_records() == existing
|
||||
end
|
||||
|
||||
test "filters blacklisted settings" do
|
||||
clear_config(:database_config_whitelist, [{:pleroma}])
|
||||
clear_config(:database_config_blacklist, [{:pleroma, :logger}, {:blacklisted_group}])
|
||||
|
||||
insert_config_record(:pleroma, :logger, backends: :console)
|
||||
insert_config_record(:web_push_encryption, :vapid_details, a: 1)
|
||||
insert_config_record(:blacklisted_group, :anything, a: 1)
|
||||
|
||||
MixTask.run(["filter_blacklisted", "--force"])
|
||||
|
||||
assert [
|
||||
%ConfigDB{group: :pleroma, key: :instance},
|
||||
%ConfigDB{group: :pleroma, key: Pleroma.Captcha},
|
||||
%ConfigDB{group: :pleroma2, key: :key2},
|
||||
%ConfigDB{group: :web_push_encryption, key: :vapid_details}
|
||||
] = config_records()
|
||||
end
|
||||
|
||||
test "filter_blacklisted doesn't crash when blacklist is unset" do
|
||||
clear_config(:database_config_blacklist, nil)
|
||||
|
||||
existing = config_records()
|
||||
MixTask.run(["filter_blacklisted", "--force"])
|
||||
assert config_records() == existing
|
||||
end
|
||||
|
||||
test "filter_blacklisted doesn't crash when blacklist is disabled" do
|
||||
clear_config(:database_config_blacklist, false)
|
||||
|
||||
existing = config_records()
|
||||
MixTask.run(["filter_blacklisted", "--force"])
|
||||
assert config_records() == existing
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Mix.Tasks.Pleroma.DatabaseTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
use Pleroma.DataCase, async: false
|
||||
use Oban.Testing, repo: Pleroma.Repo
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Bookmark
|
||||
alias Pleroma.Hashtag
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
|
@ -251,7 +252,7 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
|
|||
|> Repo.update!()
|
||||
|
||||
{:ok, old_favourite_activity} =
|
||||
CommonAPI.favorite(remote_user2, old_remote_post_activity.id)
|
||||
CommonAPI.favorite(old_remote_post_activity.id, remote_user2)
|
||||
|
||||
old_favourite_activity
|
||||
|> Ecto.Changeset.change(%{local: false, updated_at: old_insert_date})
|
||||
|
|
@ -302,7 +303,7 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
|
|||
|> Ecto.Changeset.change(%{local: false, updated_at: old_insert_date})
|
||||
|> Repo.update!()
|
||||
|
||||
{:ok, old_favourite_activity} = CommonAPI.favorite(local_user, old_remote_post3_activity.id)
|
||||
{:ok, old_favourite_activity} = CommonAPI.favorite(old_remote_post3_activity.id, local_user)
|
||||
|
||||
old_favourite_activity
|
||||
|> Ecto.Changeset.change(%{local: true, updated_at: old_insert_date})
|
||||
|
|
@ -411,7 +412,7 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
|
|||
["scheduled_activities"],
|
||||
["schema_migrations"],
|
||||
["thread_mutes"],
|
||||
# ["user_follows_hashtag"], # not in pleroma
|
||||
["user_follows_hashtag"],
|
||||
# ["user_frontend_setting_profiles"], # not in pleroma
|
||||
["user_invite_tokens"],
|
||||
["user_notes"],
|
||||
|
|
@ -550,6 +551,39 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
|
|||
|
||||
assert length(activities) == 3
|
||||
end
|
||||
|
||||
test "it prunes hashtags with no objects associated", %{old_insert_date: old_insert_date} do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, hashtag_post_activity} =
|
||||
CommonAPI.post(user, %{status: "morning #cofe", local: true})
|
||||
|
||||
hashtag_post_object = Object.normalize(hashtag_post_activity)
|
||||
|
||||
{:ok, hashtag_post2_activity} =
|
||||
CommonAPI.post(user, %{status: "morning #cawfee", local: true})
|
||||
|
||||
hashtag_post2_object = Object.normalize(hashtag_post2_activity)
|
||||
|
||||
hashtag_post_object
|
||||
|> Ecto.Changeset.change(%{updated_at: old_insert_date})
|
||||
|> Repo.update!()
|
||||
|
||||
hashtag_post2_object
|
||||
|> Ecto.Changeset.change(%{updated_at: old_insert_date})
|
||||
|> Repo.update!()
|
||||
|
||||
# Test whether hashtags with follow relationships are kept
|
||||
User.follow_hashtag(user, Hashtag.get_by_name("cofe"))
|
||||
|
||||
assert length(Repo.all(Hashtag)) == 2
|
||||
assert length(Repo.all(Object)) == 2
|
||||
|
||||
Mix.Tasks.Pleroma.Database.run(["prune_objects"])
|
||||
assert length(Repo.all(Hashtag)) == 1
|
||||
assert length(Repo.all(Object)) == 0
|
||||
assert Repo.one(Hashtag) |> Map.fetch!(:name) == "cofe"
|
||||
end
|
||||
end
|
||||
|
||||
describe "running update_users_following_followers_counts" do
|
||||
|
|
@ -586,7 +620,7 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
|
|||
{:ok, %{id: id, object: object}} = CommonAPI.post(user, %{status: "test"})
|
||||
{:ok, %{object: object2}} = CommonAPI.post(user, %{status: "test test"})
|
||||
|
||||
CommonAPI.favorite(user2, id)
|
||||
CommonAPI.favorite(id, user2)
|
||||
|
||||
likes = %{
|
||||
"first" =>
|
||||
|
|
@ -623,10 +657,12 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
|
|||
|
||||
expires_at = DateTime.add(DateTime.utc_now(), 60 * 61)
|
||||
|
||||
Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
|
||||
activity_id: activity_id3,
|
||||
expires_at: expires_at
|
||||
})
|
||||
Pleroma.Workers.PurgeExpiredActivity.enqueue(
|
||||
%{
|
||||
activity_id: activity_id3
|
||||
},
|
||||
scheduled_at: expires_at
|
||||
)
|
||||
|
||||
Mix.Tasks.Pleroma.Database.run(["ensure_expiration"])
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ defmodule Mix.Tasks.Pleroma.DigestTest do
|
|||
setup do: clear_config([Pleroma.Emails.Mailer, :enabled], true)
|
||||
|
||||
setup do
|
||||
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
|
||||
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
|
||||
:ok
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ defmodule Mix.Tasks.Pleroma.FrontendTest do
|
|||
@dir "test/frontend_static_test"
|
||||
|
||||
setup do
|
||||
File.mkdir_p!(@dir)
|
||||
Pleroma.Backports.mkdir_p!(@dir)
|
||||
clear_config([:instance, :static_dir], @dir)
|
||||
|
||||
on_exit(fn ->
|
||||
|
|
@ -50,7 +50,7 @@ defmodule Mix.Tasks.Pleroma.FrontendTest do
|
|||
|
||||
folder = Path.join([@dir, "frontends", "pleroma", "fantasy"])
|
||||
previously_existing = Path.join([folder, "temp"])
|
||||
File.mkdir_p!(folder)
|
||||
Pleroma.Backports.mkdir_p!(folder)
|
||||
File.write!(previously_existing, "yey")
|
||||
assert File.exists?(previously_existing)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ defmodule Mix.Tasks.Pleroma.InstanceTest do
|
|||
use Pleroma.DataCase
|
||||
|
||||
setup do
|
||||
File.mkdir_p!(tmp_path())
|
||||
Pleroma.Backports.mkdir_p!(tmp_path())
|
||||
|
||||
on_exit(fn ->
|
||||
File.rm_rf(tmp_path())
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Mix.Tasks.Pleroma.UploadsTest do
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Upload
|
||||
use Pleroma.DataCase
|
||||
use Pleroma.DataCase, async: false
|
||||
|
||||
import Mock
|
||||
|
||||
setup_all do
|
||||
prep_uploads()
|
||||
Mix.shell(Mix.Shell.Process)
|
||||
|
||||
on_exit(fn ->
|
||||
|
|
@ -18,6 +20,8 @@ defmodule Mix.Tasks.Pleroma.UploadsTest do
|
|||
:ok
|
||||
end
|
||||
|
||||
setup do: clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
|
||||
|
||||
describe "running migrate_local" do
|
||||
test "uploads migrated" do
|
||||
with_mock Upload,
|
||||
|
|
@ -53,4 +57,15 @@ defmodule Mix.Tasks.Pleroma.UploadsTest do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp prep_uploads do
|
||||
upload_dir = Config.get([Pleroma.Uploaders.Local, :uploads])
|
||||
|
||||
if not File.exists?(upload_dir) || File.ls!(upload_dir) == [] do
|
||||
Pleroma.Backports.mkdir_p(upload_dir)
|
||||
|
||||
Path.join([upload_dir, "file.txt"])
|
||||
|> File.touch()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ defmodule Mix.Tasks.Pleroma.UserTest do
|
|||
import Pleroma.Factory
|
||||
|
||||
setup do
|
||||
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
|
||||
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
|
||||
:ok
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -249,7 +249,7 @@ defmodule Pleroma.ActivityTest do
|
|||
{:ok, %{id: id, object: %{data: %{"id" => obj_id}}}} =
|
||||
Pleroma.Web.CommonAPI.post(user, %{status: "cofe"})
|
||||
|
||||
Pleroma.Web.CommonAPI.favorite(another, id)
|
||||
Pleroma.Web.CommonAPI.favorite(id, another)
|
||||
|
||||
assert obj_id
|
||||
|> Pleroma.Activity.Queries.by_object_id()
|
||||
|
|
@ -261,23 +261,27 @@ defmodule Pleroma.ActivityTest do
|
|||
test "add_by_params_query/3" do
|
||||
user = insert(:user)
|
||||
|
||||
note = insert(:note_activity, user: user)
|
||||
note_activity = insert(:note_activity, user: user)
|
||||
|
||||
insert(:add_activity, user: user, note: note)
|
||||
insert(:add_activity, user: user, note: note)
|
||||
insert(:add_activity, user: user, note_activity: note_activity)
|
||||
insert(:add_activity, user: user, note_activity: note_activity)
|
||||
insert(:add_activity, user: user)
|
||||
|
||||
assert Repo.aggregate(Activity, :count, :id) == 4
|
||||
assert Repo.aggregate(Activity, :count, :id) == 5
|
||||
|
||||
add_query =
|
||||
Activity.add_by_params_query(note.data["object"], user.ap_id, user.featured_address)
|
||||
Activity.add_by_params_query(
|
||||
note_activity.data["object"],
|
||||
user.ap_id,
|
||||
user.featured_address
|
||||
)
|
||||
|
||||
assert Repo.aggregate(add_query, :count, :id) == 2
|
||||
|
||||
Repo.delete_all(add_query)
|
||||
assert Repo.aggregate(add_query, :count, :id) == 0
|
||||
|
||||
assert Repo.aggregate(Activity, :count, :id) == 2
|
||||
assert Repo.aggregate(Activity, :count, :id) == 3
|
||||
end
|
||||
|
||||
describe "associated_object_id() sql function" do
|
||||
|
|
|
|||
39
test/pleroma/application_test.exs
Normal file
39
test/pleroma/application_test.exs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.ApplicationTest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
|
||||
# TODO HACK: Temporarily disabled.
|
||||
# Currently causes a bunch of warnings about :warn being deprecated in logs.
|
||||
# ex_syslogger needs to either be updated upstream or forked.
|
||||
describe "config :pleroma, :logger, backends: [{:ex_syslogger, :ex_syslogger}]" do
|
||||
setup do
|
||||
clear_config([:logger], backends: [{:ex_syslogger, :ex_syslogger}])
|
||||
end
|
||||
|
||||
@tag :skip
|
||||
test "is handled" do
|
||||
log = capture_log(fn -> Pleroma.Application.configure_logger() end)
|
||||
|
||||
assert log =~
|
||||
"Configuration {:ex_syslogger, :ex_syslogger} is incorrect. Use {ExSyslogger, :ex_syslogger} instead!"
|
||||
|
||||
assert log =~ "Successfully added logger backend: {ExSyslogger, :ex_syslogger}"
|
||||
end
|
||||
end
|
||||
|
||||
describe "config :pleroma, :logger, :backends: [:console]" do
|
||||
setup do
|
||||
clear_config([:logger], backends: [:console])
|
||||
end
|
||||
|
||||
test "emits a warning" do
|
||||
assert capture_log(fn -> Pleroma.Application.configure_logger() end) =~
|
||||
":console is no longer considered a backend and is enabled by default"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Config.DeprecationWarningsTest do
|
||||
use ExUnit.Case
|
||||
use ExUnit.Case, async: false
|
||||
use Pleroma.Tests.Helpers
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
|
|
@ -388,4 +388,62 @@ defmodule Pleroma.Config.DeprecationWarningsTest do
|
|||
end) =~
|
||||
"Your config is using the old namespace for the Shoutbox configuration."
|
||||
end
|
||||
|
||||
describe "check_deprecated_logger_config" do
|
||||
setup do
|
||||
initial_console = Application.get_env(:logger, :console, nil)
|
||||
initial_backends = Application.get_env(:logger, :backends, nil)
|
||||
|
||||
# NOTE: The Logger does not get reconfigured when running in the test env,
|
||||
# the added and on_exit removed settings below should have no effect besides
|
||||
# triggering the deprecation warnings
|
||||
Application.put_env(:logger, :console, level: :all)
|
||||
Application.put_env(:logger, :backends, [:console])
|
||||
|
||||
on_exit(fn ->
|
||||
if initial_console do
|
||||
Application.put_env(:logger, :console, initial_console)
|
||||
else
|
||||
Application.delete_env(:logger, :console)
|
||||
end
|
||||
|
||||
if initial_backends do
|
||||
Application.put_env(:logger, :backends, initial_backends)
|
||||
else
|
||||
Application.delete_env(:logger, :backends)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
test "warns on deprecated syntax" do
|
||||
log =
|
||||
capture_log(fn ->
|
||||
Pleroma.Config.DeprecationWarnings.check_deprecated_logger_config()
|
||||
end)
|
||||
|
||||
assert log =~
|
||||
"""
|
||||
!!!DEPRECATION WARNING!!!
|
||||
Your configuration is using deprecated syntax for configuring backends of Elixir's logger.
|
||||
`config :logger, backends: [...]` is deprecated syntax due to changes in Elixir.
|
||||
Please update your configuration at your earliest convenience to use:
|
||||
`config :pleroma, :logger, backends: [...]`
|
||||
|
||||
Note: `:console` is no longer considered a backend and is used by default, you can disable it using:
|
||||
`config :logger, :default_handler: false`
|
||||
"""
|
||||
|
||||
assert log =~
|
||||
"""
|
||||
!!!DEPRECATION WARNING!!!
|
||||
Your configuration is using deprecated syntax for configuring logging to console.
|
||||
`config :logger, :console` is deprecated syntax due to changes in Elixir.
|
||||
Please update your configuration at your earliest convenience to use
|
||||
`config :logger, default_handler` and `config :logger, :default_formatter`.
|
||||
|
||||
Note: `:default_handler` is used only for the `level` setting. All other configurations go under
|
||||
`:default_formatter`. For more info visit: https://hexdocs.pm/logger/Logger.html#module-backends-and-backwards-compatibility
|
||||
"""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -174,6 +174,22 @@ defmodule Pleroma.ConfigDBTest do
|
|||
assert updated1.value == [groups: [c: 3, d: 4], key: [a: 1, b: 2]]
|
||||
assert updated2.value == [mascots: [c: 3, d: 4], key: [a: 1, b: 2]]
|
||||
end
|
||||
|
||||
test "rejects invalid :rate_limit values (e.g. empty-string scale from AdminFE)" do
|
||||
assert {:error, _changeset} =
|
||||
ConfigDB.update_or_create(%{
|
||||
group: ":pleroma",
|
||||
key: ":rate_limit",
|
||||
value: [
|
||||
%{
|
||||
"tuple" => [
|
||||
":statuses_actions",
|
||||
[%{"tuple" => ["", 0]}, %{"tuple" => ["", ""]}]
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete/1" do
|
||||
|
|
@ -273,24 +289,28 @@ defmodule Pleroma.ConfigDBTest do
|
|||
end
|
||||
|
||||
test "sigil" do
|
||||
assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]") == ~r/comp[lL][aA][iI][nN]er/
|
||||
assert ConfigDB.to_elixir_types("~r[comp[lL][aA][iI][nN]er]").source ==
|
||||
~r/comp[lL][aA][iI][nN]er/.source
|
||||
end
|
||||
|
||||
test "link sigil" do
|
||||
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/") == ~r/https:\/\/example.com/
|
||||
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/").source ==
|
||||
~r/https:\/\/example.com/.source
|
||||
end
|
||||
|
||||
test "link sigil with um modifiers" do
|
||||
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/um") ==
|
||||
~r/https:\/\/example.com/um
|
||||
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/um").source ==
|
||||
~r/https:\/\/example.com/um.source
|
||||
end
|
||||
|
||||
test "link sigil with i modifier" do
|
||||
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i") == ~r/https:\/\/example.com/i
|
||||
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/i").source ==
|
||||
~r/https:\/\/example.com/i.source
|
||||
end
|
||||
|
||||
test "link sigil with s modifier" do
|
||||
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s") == ~r/https:\/\/example.com/s
|
||||
assert ConfigDB.to_elixir_types("~r/https:\/\/example.com/s").source ==
|
||||
~r/https:\/\/example.com/s.source
|
||||
end
|
||||
|
||||
test "raise if valid delimiter not found" do
|
||||
|
|
@ -312,7 +332,7 @@ defmodule Pleroma.ConfigDBTest do
|
|||
test "proxy tuple with domain" do
|
||||
assert ConfigDB.to_elixir_types(%{
|
||||
"tuple" => [":proxy_url", %{"tuple" => [":socks5", "domain.com", 1234]}]
|
||||
}) == {:proxy_url, {:socks5, 'domain.com', 1234}}
|
||||
}) == {:proxy_url, {:socks5, ~c"domain.com", 1234}}
|
||||
end
|
||||
|
||||
test "proxy tuple with ip" do
|
||||
|
|
@ -460,11 +480,11 @@ defmodule Pleroma.ConfigDBTest do
|
|||
test "complex keyword with sigil" do
|
||||
assert ConfigDB.to_elixir_types([
|
||||
%{"tuple" => [":federated_timeline_removal", []]},
|
||||
%{"tuple" => [":reject", ["~r/comp[lL][aA][iI][nN]er/"]]},
|
||||
%{"tuple" => [":reject", [~r/comp[lL][aA][iI][nN]er/.source]]},
|
||||
%{"tuple" => [":replace", []]}
|
||||
]) == [
|
||||
federated_timeline_removal: [],
|
||||
reject: [~r/comp[lL][aA][iI][nN]er/],
|
||||
reject: [~r/comp[lL][aA][iI][nN]er/.source],
|
||||
replace: []
|
||||
]
|
||||
end
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ defmodule Pleroma.Conversation.ParticipationTest do
|
|||
# When it's a reply from the blocked user
|
||||
{:ok, _direct2} =
|
||||
CommonAPI.post(blocked, %{
|
||||
status: "reply",
|
||||
status: "@#{third_user.nickname}, #{blocker.nickname} reply",
|
||||
visibility: "direct",
|
||||
in_reply_to_conversation_id: blocked_participation.id
|
||||
})
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ defmodule Pleroma.ConversationTest do
|
|||
setup_all do: clear_config([:instance, :federating], true)
|
||||
|
||||
setup do
|
||||
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
|
||||
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
|
||||
:ok
|
||||
end
|
||||
|
||||
|
|
@ -66,8 +66,10 @@ defmodule Pleroma.ConversationTest do
|
|||
jafnhar = insert(:user, local: false)
|
||||
tridi = insert(:user)
|
||||
|
||||
to = [har.nickname, jafnhar.nickname, tridi.nickname]
|
||||
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(har, %{status: "Hey @#{jafnhar.nickname}", visibility: "direct"})
|
||||
CommonAPI.post(har, %{status: "Hey @#{jafnhar.nickname}", visibility: "direct", to: to})
|
||||
|
||||
object = Pleroma.Object.normalize(activity, fetch: false)
|
||||
context = object.data["context"]
|
||||
|
|
@ -88,7 +90,8 @@ defmodule Pleroma.ConversationTest do
|
|||
CommonAPI.post(jafnhar, %{
|
||||
status: "Hey @#{har.nickname}",
|
||||
visibility: "direct",
|
||||
in_reply_to_status_id: activity.id
|
||||
in_reply_to_status_id: activity.id,
|
||||
to: to
|
||||
})
|
||||
|
||||
object = Pleroma.Object.normalize(activity, fetch: false)
|
||||
|
|
@ -112,7 +115,8 @@ defmodule Pleroma.ConversationTest do
|
|||
CommonAPI.post(tridi, %{
|
||||
status: "Hey @#{har.nickname}",
|
||||
visibility: "direct",
|
||||
in_reply_to_status_id: activity.id
|
||||
in_reply_to_status_id: activity.id,
|
||||
to: to
|
||||
})
|
||||
|
||||
object = Pleroma.Object.normalize(activity, fetch: false)
|
||||
|
|
|
|||
16
test/pleroma/dependency_version_test.exs
Normal file
16
test/pleroma/dependency_version_test.exs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.DependencyVersionTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
test "uses majic 1.2" do
|
||||
majic_version =
|
||||
:majic
|
||||
|> Application.spec(:vsn)
|
||||
|> to_string()
|
||||
|
||||
assert Version.match?(majic_version, "~> 1.2")
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.ContentLanguageMapTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators.ContentLanguageMap
|
||||
|
||||
test "it validates" do
|
||||
data = %{
|
||||
"en-US" => "mew mew",
|
||||
"en-GB" => "meow meow"
|
||||
}
|
||||
|
||||
assert {:ok, ^data} = ContentLanguageMap.cast(data)
|
||||
end
|
||||
|
||||
test "it validates empty strings" do
|
||||
data = %{
|
||||
"en-US" => "mew mew",
|
||||
"en-GB" => ""
|
||||
}
|
||||
|
||||
assert {:ok, ^data} = ContentLanguageMap.cast(data)
|
||||
end
|
||||
|
||||
test "it ignores non-strings within the map" do
|
||||
data = %{
|
||||
"en-US" => "mew mew",
|
||||
"en-GB" => 123
|
||||
}
|
||||
|
||||
assert {:ok, validated_data} = ContentLanguageMap.cast(data)
|
||||
|
||||
assert validated_data == %{"en-US" => "mew mew"}
|
||||
end
|
||||
|
||||
test "it ignores bad locale codes" do
|
||||
data = %{
|
||||
"en-US" => "mew mew",
|
||||
"en_GB" => "meow meow",
|
||||
"en<<#@!$#!@%!GB" => "meow meow"
|
||||
}
|
||||
|
||||
assert {:ok, validated_data} = ContentLanguageMap.cast(data)
|
||||
|
||||
assert validated_data == %{"en-US" => "mew mew"}
|
||||
end
|
||||
|
||||
test "it complains with non-map data" do
|
||||
assert :error = ContentLanguageMap.cast("mew")
|
||||
assert :error = ContentLanguageMap.cast(["mew"])
|
||||
assert :error = ContentLanguageMap.cast([%{"en-US" => "mew"}])
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCodeTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.EctoType.ActivityPub.ObjectValidators.LanguageCode
|
||||
|
||||
test "it accepts language code" do
|
||||
text = "pl"
|
||||
assert {:ok, ^text} = LanguageCode.cast(text)
|
||||
end
|
||||
|
||||
test "it accepts language code with region" do
|
||||
text = "pl-PL"
|
||||
assert {:ok, ^text} = LanguageCode.cast(text)
|
||||
end
|
||||
|
||||
test "errors for invalid language code" do
|
||||
assert {:error, :invalid_language} = LanguageCode.cast("ru_RU")
|
||||
assert {:error, :invalid_language} = LanguageCode.cast(" ")
|
||||
assert {:error, :invalid_language} = LanguageCode.cast("en-US\n")
|
||||
end
|
||||
|
||||
test "errors for non-text" do
|
||||
assert :error == LanguageCode.cast(42)
|
||||
end
|
||||
end
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
defmodule Pleroma.Emoji.PackTest do
|
||||
use Pleroma.DataCase
|
||||
alias Pleroma.Emoji
|
||||
alias Pleroma.Emoji.Pack
|
||||
|
||||
@emoji_path Path.join(
|
||||
|
|
@ -12,6 +13,9 @@ defmodule Pleroma.Emoji.PackTest do
|
|||
)
|
||||
|
||||
setup do
|
||||
# Reload emoji to ensure a clean state
|
||||
Emoji.reload()
|
||||
|
||||
pack_path = Path.join(@emoji_path, "dump_pack")
|
||||
File.mkdir(pack_path)
|
||||
|
||||
|
|
@ -53,6 +57,63 @@ defmodule Pleroma.Emoji.PackTest do
|
|||
|
||||
assert updated_pack.files_count == 5
|
||||
end
|
||||
|
||||
test "skips existing emojis when adding from zip file", %{pack: pack} do
|
||||
# First, let's create a test pack with a "bear" emoji
|
||||
test_pack_path = Path.join(@emoji_path, "test_bear_pack")
|
||||
Pleroma.Backports.mkdir_p(test_pack_path)
|
||||
|
||||
# Create a pack.json file
|
||||
File.write!(Path.join(test_pack_path, "pack.json"), """
|
||||
{
|
||||
"files": { "bear": "bear.png" },
|
||||
"pack": {
|
||||
"description": "Bear Pack", "homepage": "https://pleroma.social",
|
||||
"license": "Test license", "share-files": true
|
||||
}}
|
||||
""")
|
||||
|
||||
# Copy a test image to use as the bear emoji
|
||||
File.cp!(
|
||||
Path.absname("test/instance_static/emoji/test_pack/blank.png"),
|
||||
Path.join(test_pack_path, "bear.png")
|
||||
)
|
||||
|
||||
# Load the pack to register the "bear" emoji in the global registry
|
||||
{:ok, _bear_pack} = Pleroma.Emoji.Pack.load_pack("test_bear_pack")
|
||||
|
||||
# Reload emoji to make sure the bear emoji is in the global registry
|
||||
Emoji.reload()
|
||||
|
||||
# Verify that the bear emoji exists in the global registry
|
||||
assert Emoji.exist?("bear")
|
||||
|
||||
# Now try to add a zip file that contains an emoji with the same shortcode
|
||||
file = %Plug.Upload{
|
||||
content_type: "application/zip",
|
||||
filename: "emojis.zip",
|
||||
path: Path.absname("test/fixtures/emojis.zip")
|
||||
}
|
||||
|
||||
{:ok, updated_pack} = Pack.add_file(pack, nil, nil, file)
|
||||
|
||||
# Verify that the "bear" emoji was skipped
|
||||
refute Map.has_key?(updated_pack.files, "bear")
|
||||
|
||||
# Other emojis should be added
|
||||
assert Map.has_key?(updated_pack.files, "a_trusted_friend-128")
|
||||
assert Map.has_key?(updated_pack.files, "auroraborealis")
|
||||
assert Map.has_key?(updated_pack.files, "baby_in_a_box")
|
||||
assert Map.has_key?(updated_pack.files, "bear-128")
|
||||
|
||||
# Total count should be 4 (all emojis except "bear")
|
||||
assert updated_pack.files_count == 4
|
||||
|
||||
# Clean up the test pack
|
||||
on_exit(fn ->
|
||||
File.rm_rf!(test_pack_path)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
test "returns error when zip file is bad", %{pack: pack} do
|
||||
|
|
@ -62,7 +123,7 @@ defmodule Pleroma.Emoji.PackTest do
|
|||
path: Path.absname("test/instance_static/emoji/test_pack/blank.png")
|
||||
}
|
||||
|
||||
assert Pack.add_file(pack, nil, nil, file) == {:error, :einval}
|
||||
assert {:error, _} = Pack.add_file(pack, nil, nil, file)
|
||||
end
|
||||
|
||||
test "returns pack when zip file is empty", %{pack: pack} do
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ defmodule Pleroma.FrontendTest do
|
|||
@dir "test/frontend_static_test"
|
||||
|
||||
setup do
|
||||
File.mkdir_p!(@dir)
|
||||
Pleroma.Backports.mkdir_p!(@dir)
|
||||
clear_config([:instance, :static_dir], @dir)
|
||||
|
||||
on_exit(fn ->
|
||||
|
|
@ -46,7 +46,7 @@ defmodule Pleroma.FrontendTest do
|
|||
|
||||
folder = Path.join([@dir, "frontends", "pleroma", "fantasy"])
|
||||
previously_existing = Path.join([folder, "temp"])
|
||||
File.mkdir_p!(folder)
|
||||
Pleroma.Backports.mkdir_p!(folder)
|
||||
File.write!(previously_existing, "yey")
|
||||
assert File.exists?(previously_existing)
|
||||
|
||||
|
|
|
|||
179
test/pleroma/gun/conn_test.exs
Normal file
179
test/pleroma/gun/conn_test.exs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Gun.ConnTest do
|
||||
use ExUnit.Case
|
||||
|
||||
import Mox
|
||||
|
||||
alias Pleroma.Gun.Conn
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
setup do
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http} end)
|
||||
stub(Pleroma.GunMock, :await_tunnel_up, fn _, _, _ -> {:ok, :http2} end)
|
||||
stub(Pleroma.GunMock, :close, fn _ -> :ok end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "selects the direct transport from the URI scheme" do
|
||||
expect(Pleroma.GunMock, :open, 2, fn
|
||||
~c"example.com", 8443, %{transport: :tls, tls_opts: tls_opts} ->
|
||||
assert tls_opts[:verify] == :verify_peer
|
||||
{:ok, spawn_link(fn -> Process.sleep(:infinity) end)}
|
||||
|
||||
~c"example.com", 443, %{transport: :tcp} ->
|
||||
{:ok, spawn_link(fn -> Process.sleep(:infinity) end)}
|
||||
end)
|
||||
|
||||
assert {:ok, https_conn, :http, nil} = Conn.open(URI.parse("https://example.com:8443"), [])
|
||||
assert {:ok, http_conn, :http, nil} = Conn.open(URI.parse("http://example.com:443"), [])
|
||||
|
||||
stop_conn(https_conn)
|
||||
stop_conn(http_conn)
|
||||
end
|
||||
|
||||
test "passes authentication to an HTTP CONNECT proxy" do
|
||||
conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||
stream = make_ref()
|
||||
|
||||
expect(Pleroma.GunMock, :open, fn ~c"proxy.example", 8080, opts ->
|
||||
assert opts.transport == :tcp
|
||||
refute Map.has_key?(opts, :tls_opts)
|
||||
{:ok, conn}
|
||||
end)
|
||||
|
||||
expect(Pleroma.GunMock, :connect, fn ^conn, connect_opts ->
|
||||
assert connect_opts.username == "alice"
|
||||
assert connect_opts.password == "secret"
|
||||
assert connect_opts.transport == :tls
|
||||
assert connect_opts.protocols == [:http2, :http]
|
||||
stream
|
||||
end)
|
||||
|
||||
expect(Pleroma.GunMock, :await, fn ^conn, ^stream -> {:response, :fin, 200, []} end)
|
||||
|
||||
assert {:ok, ^conn, :http2, ^stream} =
|
||||
Conn.open(URI.parse("https://origin.example/inbox"),
|
||||
proxy: {~c"proxy.example", 8080},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
)
|
||||
|
||||
stop_conn(conn)
|
||||
end
|
||||
|
||||
test "uses separate TLS options for a TLS CONNECT proxy" do
|
||||
conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||
stream = make_ref()
|
||||
|
||||
expect(Pleroma.GunMock, :open, fn ~c"proxy.example", 8443, opts ->
|
||||
assert opts.transport == :tls
|
||||
assert opts.tls_opts == [verify: :verify_none]
|
||||
{:ok, conn}
|
||||
end)
|
||||
|
||||
expect(Pleroma.GunMock, :connect, fn ^conn, _connect_opts -> stream end)
|
||||
expect(Pleroma.GunMock, :await, fn ^conn, ^stream -> {:response, :fin, 200, []} end)
|
||||
|
||||
assert {:ok, ^conn, :http2, ^stream} =
|
||||
Conn.open(URI.parse("https://origin.example/inbox"),
|
||||
proxy: {~c"proxy.example", 8443},
|
||||
transport: :tls,
|
||||
proxy_tls_opts: [verify: :verify_none]
|
||||
)
|
||||
|
||||
stop_conn(conn)
|
||||
end
|
||||
|
||||
test "uses HTTP/1 for a TLS forwarding proxy" do
|
||||
conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||
|
||||
expect(Pleroma.GunMock, :open, fn ~c"proxy.example", 8443, opts ->
|
||||
assert opts.transport == :tls
|
||||
assert opts.tls_opts == [verify: :verify_none]
|
||||
assert opts.protocols == [:http]
|
||||
{:ok, conn}
|
||||
end)
|
||||
|
||||
assert {:ok, ^conn, :http, :forward_proxy} =
|
||||
Conn.open(URI.parse("http://origin.example/inbox"),
|
||||
proxy: {~c"proxy.example", 8443},
|
||||
transport: :tls,
|
||||
proxy_tls_opts: [verify: :verify_none]
|
||||
)
|
||||
|
||||
stop_conn(conn)
|
||||
end
|
||||
|
||||
test "passes authentication and remote DNS destination to a SOCKS5 proxy" do
|
||||
conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||
|
||||
expect(Pleroma.GunMock, :open, fn ~c"proxy.example", 1080, opts ->
|
||||
assert opts.protocols == [:socks]
|
||||
assert opts.transport == :tcp
|
||||
assert opts.socks_opts.host == ~c"origin.example"
|
||||
assert opts.socks_opts.port == 443
|
||||
assert opts.socks_opts.auth == [{:username_password, "alice", "secret"}]
|
||||
assert opts.socks_opts.transport == :tls
|
||||
assert opts.socks_opts.protocols == [:http2, :http]
|
||||
refute Map.has_key?(opts, :tls_opts)
|
||||
{:ok, conn}
|
||||
end)
|
||||
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :socks} end)
|
||||
|
||||
assert {:ok, ^conn, :http2, nil} =
|
||||
Conn.open(URI.parse("https://origin.example/inbox"),
|
||||
proxy: {:socks5, ~c"proxy.example", 1080},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
)
|
||||
|
||||
stop_conn(conn)
|
||||
end
|
||||
|
||||
test "closes a CONNECT socket when proxy authentication fails" do
|
||||
conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||
stream = make_ref()
|
||||
|
||||
expect(Pleroma.GunMock, :open, fn _, _, _ -> {:ok, conn} end)
|
||||
expect(Pleroma.GunMock, :connect, fn ^conn, _ -> stream end)
|
||||
expect(Pleroma.GunMock, :await, fn ^conn, ^stream -> {:response, :nofin, 407, []} end)
|
||||
expect(Pleroma.GunMock, :close, fn ^conn -> Process.exit(conn, :normal) end)
|
||||
|
||||
assert {:error, :proxy_auth_failed} =
|
||||
Conn.open(URI.parse("https://origin.example/inbox"),
|
||||
proxy: {~c"proxy.example", 8080},
|
||||
proxy_auth: {"alice", "wrong"}
|
||||
)
|
||||
end
|
||||
|
||||
test "returns the proxy status when CONNECT is forbidden" do
|
||||
conn = spawn_link(fn -> Process.sleep(:infinity) end)
|
||||
stream = make_ref()
|
||||
|
||||
expect(Pleroma.GunMock, :open, fn _, _, _ -> {:ok, conn} end)
|
||||
expect(Pleroma.GunMock, :connect, fn ^conn, _ -> stream end)
|
||||
expect(Pleroma.GunMock, :await, fn ^conn, ^stream -> {:response, :fin, 403, []} end)
|
||||
expect(Pleroma.GunMock, :close, fn ^conn -> Process.exit(conn, :normal) end)
|
||||
|
||||
assert {:error, {:proxy_connect_failed, 403}} =
|
||||
Conn.open(URI.parse("https://origin.example/inbox"),
|
||||
proxy: {~c"proxy.example", 8080}
|
||||
)
|
||||
end
|
||||
|
||||
test "rejects SOCKS4 before opening a socket" do
|
||||
assert {:error, :socks4_unsupported} =
|
||||
Conn.open(URI.parse("https://origin.example/inbox"),
|
||||
proxy: {:socks4, ~c"proxy.example", 1080}
|
||||
)
|
||||
end
|
||||
|
||||
defp stop_conn(conn) do
|
||||
Process.unlink(conn)
|
||||
Process.exit(conn, :kill)
|
||||
end
|
||||
end
|
||||
|
|
@ -11,8 +11,11 @@ defmodule Pleroma.Gun.ConnectionPoolTest do
|
|||
|
||||
defp gun_mock(_) do
|
||||
Pleroma.GunMock
|
||||
|> stub(:open, fn _, _, _ -> Task.start_link(fn -> Process.sleep(100) end) end)
|
||||
|> stub(:open, fn _, _, _ -> Task.start_link(fn -> Process.sleep(:infinity) end) end)
|
||||
|> stub(:await_up, fn _, _ -> {:ok, :http} end)
|
||||
|> stub(:connect, fn _, _ -> make_ref() end)
|
||||
|> stub(:await, fn _, _ -> {:response, :fin, 200, []} end)
|
||||
|> stub(:await_tunnel_up, fn _, _, _ -> {:ok, :http2} end)
|
||||
|> stub(:set_owner, fn _, _ -> :ok end)
|
||||
|
||||
:ok
|
||||
|
|
@ -20,41 +23,213 @@ defmodule Pleroma.Gun.ConnectionPoolTest do
|
|||
|
||||
setup :gun_mock
|
||||
|
||||
test "gives the same connection to 2 concurrent requests" do
|
||||
Enum.map(
|
||||
[
|
||||
"http://www.korean-books.com.kp/KBMbooks/en/periodic/pictorial/20200530163914.pdf",
|
||||
"http://www.korean-books.com.kp/KBMbooks/en/periodic/pictorial/20200528183427.pdf"
|
||||
],
|
||||
fn uri ->
|
||||
uri = URI.parse(uri)
|
||||
task_parent = self()
|
||||
test "opens sibling HTTP/1 connections while an existing connection is in use" do
|
||||
uri = URI.parse("http://www.korean-books.com.kp/document.pdf")
|
||||
|
||||
Task.start_link(fn ->
|
||||
{:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
ConnectionPool.release_conn(conn)
|
||||
send(task_parent, conn)
|
||||
end)
|
||||
end
|
||||
)
|
||||
assert {:ok, first} = ConnectionPool.get_conn(uri, [])
|
||||
assert {:ok, second} = ConnectionPool.get_conn(uri, [])
|
||||
assert first != second
|
||||
|
||||
[pid, pid] =
|
||||
for _ <- 1..2 do
|
||||
receive do
|
||||
pid -> pid
|
||||
end
|
||||
end
|
||||
assert :ok = ConnectionPool.release_conn(first)
|
||||
assert :ok = ConnectionPool.release_conn(second)
|
||||
end
|
||||
|
||||
test "multiplexes concurrent HTTP/2 leases on one connection" do
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http2} end)
|
||||
uri = URI.parse("https://h2.example/inbox")
|
||||
|
||||
assert {:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
assert {:ok, ^conn} = ConnectionPool.get_conn(uri, [])
|
||||
|
||||
assert :ok = ConnectionPool.release_conn(conn)
|
||||
assert :ok = ConnectionPool.release_conn(conn)
|
||||
assert Process.alive?(conn)
|
||||
end
|
||||
|
||||
test "does not share connections across direct and authenticated proxy routes" do
|
||||
uri = URI.parse("https://routes.example/inbox")
|
||||
|
||||
assert {:ok, direct} = ConnectionPool.get_conn(uri, [])
|
||||
|
||||
assert {:ok, proxied} =
|
||||
ConnectionPool.get_conn(uri,
|
||||
proxy: {~c"proxy.example", 8080},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
)
|
||||
|
||||
assert {:ok, other_credentials} =
|
||||
ConnectionPool.get_conn(uri,
|
||||
proxy: {~c"proxy.example", 8080},
|
||||
proxy_auth: {"bob", "different-secret"}
|
||||
)
|
||||
|
||||
assert Enum.uniq([direct, proxied, other_credentials]) ==
|
||||
[direct, proxied, other_credentials]
|
||||
|
||||
registry_dump = inspect(Registry.select(ConnectionPool, [{{:"$1", :_, :_}, [], [:"$1"]}]))
|
||||
refute registry_dump =~ "secret"
|
||||
|
||||
Enum.each([direct, proxied, other_credentials], &ConnectionPool.release_conn/1)
|
||||
end
|
||||
|
||||
test "coalesces concurrent cold HTTP/2 checkouts behind one connection" do
|
||||
test_pid = self()
|
||||
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ ->
|
||||
send(test_pid, {:awaiting_connection, self()})
|
||||
|
||||
receive do
|
||||
:continue -> {:ok, :http2}
|
||||
end
|
||||
end)
|
||||
|
||||
uri = URI.parse("https://cold-h2.example/inbox")
|
||||
|
||||
tasks =
|
||||
for _ <- 1..10 do
|
||||
Task.async(fn -> ConnectionPool.get_conn(uri, []) end)
|
||||
end
|
||||
|
||||
assert_receive {:awaiting_connection, worker_pid}
|
||||
refute_receive {:awaiting_connection, _other_worker}, 50
|
||||
send(worker_pid, :continue)
|
||||
|
||||
assert [{:ok, conn}] = tasks |> Enum.map(&Task.await/1) |> Enum.uniq()
|
||||
assert Process.alive?(conn)
|
||||
end
|
||||
|
||||
test "shares a cold connection failure with concurrent waiters" do
|
||||
test_pid = self()
|
||||
|
||||
stub(Pleroma.GunMock, :open, fn _, _, _ ->
|
||||
send(test_pid, {:opening_connection, self()})
|
||||
|
||||
receive do
|
||||
:fail -> {:error, :econnrefused}
|
||||
end
|
||||
end)
|
||||
|
||||
uri = URI.parse("https://unreachable.example/inbox")
|
||||
|
||||
tasks =
|
||||
for _ <- 1..10 do
|
||||
Task.async(fn -> ConnectionPool.get_conn(uri, []) end)
|
||||
end
|
||||
|
||||
assert_receive {:opening_connection, worker_pid}
|
||||
refute_receive {:opening_connection, _other_worker}, 50
|
||||
send(worker_pid, :fail)
|
||||
|
||||
assert Enum.all?(tasks, fn task -> match?({:error, _reason}, Task.await(task)) end)
|
||||
refute_receive {:opening_connection, _other_worker}, 50
|
||||
end
|
||||
|
||||
test "bounds many-host workloads and closes released idle workers" do
|
||||
clear_config([:connections_pool, :max_connections]) do
|
||||
clear_config([:connections_pool, :max_connections], 25)
|
||||
clear_config([:connections_pool, :max_idle_time], 5)
|
||||
restart_worker_supervisor()
|
||||
|
||||
on_exit(&restart_worker_supervisor/0)
|
||||
end
|
||||
|
||||
connections =
|
||||
Enum.map(1..25, fn host_number ->
|
||||
uri = URI.parse("https://host-#{host_number}.example/inbox")
|
||||
assert {:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
conn
|
||||
end)
|
||||
|
||||
assert DynamicSupervisor.count_children(Pleroma.Gun.ConnectionPool.WorkerSupervisor).active ==
|
||||
25
|
||||
|
||||
assert {:error, :pool_full} =
|
||||
ConnectionPool.get_conn(URI.parse("https://overflow.example/inbox"), [])
|
||||
|
||||
Enum.each(connections, &ConnectionPool.release_conn/1)
|
||||
|
||||
assert eventually(fn ->
|
||||
DynamicSupervisor.count_children(Pleroma.Gun.ConnectionPool.WorkerSupervisor).active ==
|
||||
0
|
||||
end)
|
||||
end
|
||||
|
||||
test "does not reclaim a worker that became active after idle selection" do
|
||||
uri = URI.parse("https://reclaim-race.example/inbox")
|
||||
|
||||
assert {:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
assert :ok = ConnectionPool.release_conn(conn)
|
||||
worker = worker_for_conn(conn)
|
||||
|
||||
assert {:idle, _crf, _last_reference} = GenServer.call(worker, :reclaim_info)
|
||||
assert {:ok, ^conn} = ConnectionPool.get_conn(uri, [])
|
||||
assert :in_use = GenServer.call(worker, :reclaim)
|
||||
assert Process.alive?(worker)
|
||||
|
||||
assert :ok = ConnectionPool.release_conn(conn)
|
||||
end
|
||||
|
||||
test "cancels one HTTP/2 stream without discarding the connection" do
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http2} end)
|
||||
uri = URI.parse("https://cancel-h2.example/inbox")
|
||||
stream = make_ref()
|
||||
|
||||
expect(Pleroma.GunMock, :cancel, fn conn, ^stream when is_pid(conn) -> :ok end)
|
||||
|
||||
assert {:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
assert :ok = ConnectionPool.register_stream(conn, stream)
|
||||
assert :ok = ConnectionPool.cancel_stream(conn, stream)
|
||||
assert {:ok, ^conn} = ConnectionPool.get_conn(uri, [])
|
||||
assert :ok = ConnectionPool.release_conn(conn)
|
||||
end
|
||||
|
||||
test "ignores a stale Gun error after cancelling an HTTP/2 stream" do
|
||||
stub(Pleroma.GunMock, :await_up, fn _, _ -> {:ok, :http2} end)
|
||||
uri = URI.parse("https://stale-cancel-h2.example/inbox")
|
||||
stream = make_ref()
|
||||
|
||||
expect(Pleroma.GunMock, :cancel, fn conn, ^stream when is_pid(conn) -> :ok end)
|
||||
|
||||
assert {:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
worker = worker_for_conn(conn)
|
||||
assert :ok = ConnectionPool.register_stream(conn, stream)
|
||||
assert :ok = ConnectionPool.cancel_stream(conn, stream)
|
||||
|
||||
send(worker, {:gun_error, conn, stream, {:badstate, "The stream cannot be found."}})
|
||||
assert %{clients: %{}} = :sys.get_state(worker)
|
||||
assert Process.alive?(worker)
|
||||
|
||||
assert {:ok, ^conn} = ConnectionPool.get_conn(uri, [])
|
||||
assert :ok = ConnectionPool.release_conn(conn)
|
||||
end
|
||||
|
||||
test "stops an HTTP/1 worker when a client dies with an active stream" do
|
||||
uri = URI.parse("http://dead-stream.example/media")
|
||||
parent = self()
|
||||
|
||||
client =
|
||||
spawn(fn ->
|
||||
{:ok, conn} = ConnectionPool.get_conn(uri, [])
|
||||
:ok = ConnectionPool.register_stream(conn, make_ref())
|
||||
send(parent, {:active_stream, conn})
|
||||
Process.sleep(:infinity)
|
||||
end)
|
||||
|
||||
assert_receive {:active_stream, conn}
|
||||
worker = worker_for_conn(conn)
|
||||
Process.exit(client, :kill)
|
||||
|
||||
assert eventually(fn -> not Process.alive?(worker) end)
|
||||
end
|
||||
|
||||
@tag :erratic
|
||||
test "connection limit is respected with concurrent requests" do
|
||||
clear_config([:connections_pool, :max_connections]) do
|
||||
clear_config([:connections_pool, :max_connections], 1)
|
||||
# The supervisor needs a reboot to apply the new config setting
|
||||
Process.exit(Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor), :kill)
|
||||
restart_worker_supervisor()
|
||||
|
||||
on_exit(fn ->
|
||||
Process.exit(Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor), :kill)
|
||||
restart_worker_supervisor()
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
@ -97,4 +272,41 @@ defmodule Pleroma.Gun.ConnectionPoolTest do
|
|||
|> Enum.sort()
|
||||
end)
|
||||
end
|
||||
|
||||
defp restart_worker_supervisor do
|
||||
supervisor = Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor)
|
||||
monitor = Process.monitor(supervisor)
|
||||
Process.exit(supervisor, :kill)
|
||||
|
||||
assert_receive {:DOWN, ^monitor, :process, ^supervisor, :killed}
|
||||
|
||||
assert eventually(fn ->
|
||||
case Process.whereis(Pleroma.Gun.ConnectionPool.WorkerSupervisor) do
|
||||
pid when is_pid(pid) -> pid != supervisor and Process.alive?(pid)
|
||||
nil -> false
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp worker_for_conn(conn) do
|
||||
[worker] =
|
||||
Registry.select(ConnectionPool, [
|
||||
{{:_, :"$1", {:"$2", :_}}, [{:==, :"$2", conn}], [:"$1"]}
|
||||
])
|
||||
|
||||
worker
|
||||
end
|
||||
|
||||
defp eventually(predicate, attempts \\ 50)
|
||||
|
||||
defp eventually(_predicate, 0), do: false
|
||||
|
||||
defp eventually(predicate, attempts) do
|
||||
if predicate.() do
|
||||
true
|
||||
else
|
||||
Process.sleep(10)
|
||||
eventually(predicate, attempts - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,4 +14,133 @@ defmodule Pleroma.HashtagTest do
|
|||
assert {:name, {"can't be blank", [validation: :required]}} in changeset.errors
|
||||
end
|
||||
end
|
||||
|
||||
describe "search_hashtags" do
|
||||
test "searches hashtags by partial match" do
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("car")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("racecar")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("nascar")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("bicycle")
|
||||
|
||||
results = Hashtag.search("car")
|
||||
assert "car" in results
|
||||
assert "racecar" in results
|
||||
assert "nascar" in results
|
||||
refute "bicycle" in results
|
||||
|
||||
results = Hashtag.search("race")
|
||||
assert "racecar" in results
|
||||
refute "car" in results
|
||||
refute "nascar" in results
|
||||
refute "bicycle" in results
|
||||
|
||||
results = Hashtag.search("nonexistent")
|
||||
assert results == []
|
||||
end
|
||||
|
||||
test "searches hashtags by multiple words in query" do
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("computer")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("laptop")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("desktop")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("phone")
|
||||
|
||||
# Search for "new computer" - should return "computer"
|
||||
results = Hashtag.search("new computer")
|
||||
assert "computer" in results
|
||||
refute "laptop" in results
|
||||
refute "desktop" in results
|
||||
refute "phone" in results
|
||||
|
||||
# Search for "computer laptop" - should return both
|
||||
results = Hashtag.search("computer laptop")
|
||||
assert "computer" in results
|
||||
assert "laptop" in results
|
||||
refute "desktop" in results
|
||||
refute "phone" in results
|
||||
|
||||
# Search for "new phone" - should return "phone"
|
||||
results = Hashtag.search("new phone")
|
||||
assert "phone" in results
|
||||
refute "computer" in results
|
||||
refute "laptop" in results
|
||||
refute "desktop" in results
|
||||
end
|
||||
|
||||
test "supports pagination" do
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("alpha")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("beta")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("gamma")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("delta")
|
||||
|
||||
results = Hashtag.search("a", limit: 2)
|
||||
assert length(results) == 2
|
||||
|
||||
results = Hashtag.search("a", limit: 2, offset: 1)
|
||||
assert length(results) == 2
|
||||
end
|
||||
|
||||
test "handles matching many search terms" do
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("computer")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("laptop")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("phone")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("tablet")
|
||||
|
||||
results = Hashtag.search("new fast computer laptop phone tablet device")
|
||||
assert "computer" in results
|
||||
assert "laptop" in results
|
||||
assert "phone" in results
|
||||
assert "tablet" in results
|
||||
end
|
||||
|
||||
test "ranks results by match quality" do
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("my_computer")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("computer_science")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("computer")
|
||||
|
||||
results = Hashtag.search("computer")
|
||||
|
||||
# Exact match first
|
||||
assert Enum.at(results, 0) == "computer"
|
||||
|
||||
# Prefix match would be next
|
||||
assert Enum.at(results, 1) == "computer_science"
|
||||
|
||||
# worst match is last
|
||||
assert Enum.at(results, 2) == "my_computer"
|
||||
end
|
||||
|
||||
test "prioritizes shorter names when ranking is equal" do
|
||||
# Create hashtags with same ranking but different lengths
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("car")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("racecar")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("nascar")
|
||||
|
||||
# Search for "car" - shorter names should come first
|
||||
results = Hashtag.search("car")
|
||||
# Shortest exact match first
|
||||
assert Enum.at(results, 0) == "car"
|
||||
assert "racecar" in results
|
||||
assert "nascar" in results
|
||||
end
|
||||
|
||||
test "handles hashtag symbols in search query" do
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("computer")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("laptop")
|
||||
{:ok, _} = Hashtag.get_or_create_by_name("phone")
|
||||
|
||||
results_with_hash = Hashtag.search("#computer #laptop")
|
||||
results_without_hash = Hashtag.search("computer laptop")
|
||||
|
||||
assert results_with_hash == results_without_hash
|
||||
|
||||
results_mixed = Hashtag.search("#computer laptop #phone")
|
||||
assert "computer" in results_mixed
|
||||
assert "laptop" in results_mixed
|
||||
assert "phone" in results_mixed
|
||||
|
||||
results_only_hash = Hashtag.search("#computer")
|
||||
results_no_hash = Hashtag.search("computer")
|
||||
assert results_only_hash == results_no_hash
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ defmodule Pleroma.HTMLTest do
|
|||
<span class="h-card"><a class="u-url mention animate-spin">@<span>foo</span></a></span>
|
||||
"""
|
||||
|
||||
@mention_hashtags_sample """
|
||||
<a href="https://mastodon.example/tags/linux" class="mention hashtag" rel="tag">#<span>linux</span></a>
|
||||
"""
|
||||
|
||||
describe "StripTags scrubber" do
|
||||
test "works as expected" do
|
||||
expected = """
|
||||
|
|
@ -126,6 +130,15 @@ defmodule Pleroma.HTMLTest do
|
|||
Pleroma.HTML.Scrubber.TwitterText
|
||||
)
|
||||
end
|
||||
|
||||
test "does allow mention hashtags" do
|
||||
expected = """
|
||||
<a href="https://mastodon.example/tags/linux" class="mention hashtag" rel="tag">#<span>linux</span></a>
|
||||
"""
|
||||
|
||||
assert expected ==
|
||||
HTML.filter_tags(@mention_hashtags_sample, Pleroma.HTML.Scrubber.Default)
|
||||
end
|
||||
end
|
||||
|
||||
describe "default scrubber" do
|
||||
|
|
@ -189,6 +202,15 @@ defmodule Pleroma.HTMLTest do
|
|||
Pleroma.HTML.Scrubber.Default
|
||||
)
|
||||
end
|
||||
|
||||
test "does allow mention hashtags" do
|
||||
expected = """
|
||||
<a href="https://mastodon.example/tags/linux" class="mention hashtag" rel="tag">#<span>linux</span></a>
|
||||
"""
|
||||
|
||||
assert expected ==
|
||||
HTML.filter_tags(@mention_hashtags_sample, Pleroma.HTML.Scrubber.Default)
|
||||
end
|
||||
end
|
||||
|
||||
describe "extract_first_external_url_from_object" do
|
||||
|
|
|
|||
|
|
@ -55,23 +55,29 @@ defmodule Pleroma.HTTP.AdapterHelper.GunTest do
|
|||
|
||||
uri = URI.parse("https://some-domain.com")
|
||||
opts = Gun.options([receive_conn: false], uri)
|
||||
assert opts[:proxy] == {'localhost', 8123}
|
||||
assert opts[:proxy] == {~c"localhost", 8123}
|
||||
end
|
||||
|
||||
test "parses tuple proxy scheme host and port" do
|
||||
clear_config([:http, :proxy_url], {:socks, 'localhost', 1234})
|
||||
clear_config([:http, :proxy_url], {:socks, ~c"localhost", 1234})
|
||||
|
||||
uri = URI.parse("https://some-domain.com")
|
||||
opts = Gun.options([receive_conn: false], uri)
|
||||
assert opts[:proxy] == {:socks, 'localhost', 1234}
|
||||
assert opts[:proxy] == {:socks, ~c"localhost", 1234}
|
||||
end
|
||||
|
||||
test "passed opts have more weight than defaults" do
|
||||
clear_config([:http, :proxy_url], {:socks5, 'localhost', 1234})
|
||||
clear_config([:http, :proxy_url], {:socks5, ~c"localhost", 1234})
|
||||
uri = URI.parse("https://some-domain.com")
|
||||
opts = Gun.options([receive_conn: false, proxy: {'example.com', 4321}], uri)
|
||||
opts = Gun.options([receive_conn: false, proxy: {~c"example.com", 4321}], uri)
|
||||
|
||||
assert opts[:proxy] == {'example.com', 4321}
|
||||
assert opts[:proxy] == {~c"example.com", 4321}
|
||||
end
|
||||
|
||||
test "uses managed chunks for streamed responses" do
|
||||
opts = Gun.options([stream: true], URI.parse("https://example.com"))
|
||||
|
||||
assert opts[:body_as] == :chunks
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,6 +16,14 @@ defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do
|
|||
describe "options/2" do
|
||||
setup do: clear_config([:http, :adapter], a: 1, b: 2)
|
||||
|
||||
test "uses redirect-safe defaults", %{uri: uri} do
|
||||
opts = Hackney.options([], uri)
|
||||
|
||||
assert opts[:follow_redirect] == false
|
||||
assert opts[:force_redirect] == false
|
||||
assert opts[:with_body] == true
|
||||
end
|
||||
|
||||
test "add proxy and opts from config", %{uri: uri} do
|
||||
opts = Hackney.options([proxy: "localhost:8123"], uri)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,12 +17,12 @@ defmodule Pleroma.HTTP.AdapterHelperTest do
|
|||
end
|
||||
|
||||
test "localhost with port" do
|
||||
assert AdapterHelper.format_proxy("localhost:8123") == {'localhost', 8123}
|
||||
assert AdapterHelper.format_proxy("localhost:8123") == {~c"localhost", 8123}
|
||||
end
|
||||
|
||||
test "tuple" do
|
||||
assert AdapterHelper.format_proxy({:socks4, :localhost, 9050}) ==
|
||||
{:socks4, 'localhost', 9050}
|
||||
{:socks4, ~c"localhost", 9050}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
808
test/pleroma/http/gun_integration_test.exs
Normal file
808
test/pleroma/http/gun_integration_test.exs
Normal file
|
|
@ -0,0 +1,808 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.GunIntegrationTest do
|
||||
use ExUnit.Case, async: false
|
||||
use Pleroma.Tests.Helpers
|
||||
|
||||
alias Pleroma.Tesla.Middleware.ConnectionPool
|
||||
|
||||
setup do
|
||||
clear_config([Pleroma.Gun], Pleroma.Gun.API)
|
||||
clear_config([:connections_pool, :max_idle_time], 1_000)
|
||||
|
||||
origin = start_tls_origin(self())
|
||||
connect_proxy = start_connect_proxy(self())
|
||||
forward_proxy = start_forward_proxy(self())
|
||||
socks_proxy = start_socks5_proxy(self())
|
||||
|
||||
on_exit(fn ->
|
||||
Process.sleep(20)
|
||||
stop_server(socks_proxy)
|
||||
stop_server(forward_proxy)
|
||||
stop_server(connect_proxy)
|
||||
stop_server(origin)
|
||||
end)
|
||||
|
||||
{:ok,
|
||||
origin: origin,
|
||||
connect_proxy: connect_proxy,
|
||||
forward_proxy: forward_proxy,
|
||||
socks_proxy: socks_proxy}
|
||||
end
|
||||
|
||||
test "requests HTTPS directly on a non-standard port", %{origin: origin} do
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(origin.base_url <> "/final")
|
||||
end
|
||||
|
||||
test "follows redirects through an authenticated CONNECT proxy", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
|
||||
request(origin.base_url <> "/redirect", proxy_opts, true)
|
||||
|
||||
expected = "Basic " <> Base.encode64("alice:secret")
|
||||
assert_receive {:connect_proxy, ^expected, "127.0.0.1", origin_port}
|
||||
assert origin_port == origin.port
|
||||
end
|
||||
|
||||
test "requests through an authenticated CONNECT proxy", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
result = request(origin.base_url <> "/final", proxy_opts)
|
||||
|
||||
expected = "Basic " <> Base.encode64("alice:secret")
|
||||
assert_receive {:connect_proxy, ^expected, "127.0.0.1", origin_port}
|
||||
assert origin_port == origin.port
|
||||
assert worker_protocol(origin.port) == :http
|
||||
assert_receive {:origin_request, "/final"}
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = result
|
||||
end
|
||||
|
||||
test "requests HTTP through a forwarding proxy", %{forward_proxy: proxy} do
|
||||
proxy_opts = [proxy: {~c"127.0.0.1", proxy.port}]
|
||||
url = "http://origin.example:8080/final?key=value"
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(url, proxy_opts)
|
||||
|
||||
assert_receive {:forward_proxy, ^url, "origin.example:8080", nil}
|
||||
end
|
||||
|
||||
test "authenticates HTTP requests to a forwarding proxy", %{forward_proxy: proxy} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
url = "http://origin.example/final"
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(url, proxy_opts)
|
||||
|
||||
expected = "Basic " <> Base.encode64("alice:secret")
|
||||
assert_receive {:forward_proxy, ^url, "origin.example", ^expected}
|
||||
end
|
||||
|
||||
test "opens a request-ready authenticated CONNECT tunnel", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
uri = URI.parse(origin.base_url <> "/final")
|
||||
|
||||
opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"},
|
||||
tls_opts: [verify: :verify_none],
|
||||
connect_timeout: 2_000
|
||||
]
|
||||
|
||||
assert {:ok, conn, :http, tunnel} = Pleroma.Gun.Conn.open(uri, opts)
|
||||
stream = :gun.get(conn, "/final", [], %{tunnel: tunnel})
|
||||
assert {:response, :nofin, 200, _headers} = :gun.await(conn, stream, 2_000)
|
||||
assert {:ok, "ok"} = :gun.await_body(conn, stream, 2_000)
|
||||
:gun.close(conn)
|
||||
end
|
||||
|
||||
test "streams chunks through an authenticated CONNECT tunnel", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: client}} =
|
||||
request_chunks(origin.base_url <> "/chunks", proxy_opts)
|
||||
|
||||
assert collect_chunks(client) == "hello"
|
||||
assert :ok = Pleroma.Gun.ConnectionPool.release_conn(client.pid)
|
||||
end
|
||||
|
||||
test "discards an unfinished HTTP/1 redirect stream before following it", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: client}} =
|
||||
request_chunks(origin.base_url <> "/redirect_chunks", proxy_opts, true)
|
||||
|
||||
assert collect_chunks(client) == "hello"
|
||||
assert :ok = Pleroma.Gun.ConnectionPool.release_conn(client.pid)
|
||||
|
||||
expected = "Basic " <> Base.encode64("alice:secret")
|
||||
assert_receive {:connect_proxy, ^expected, "127.0.0.1", _port}
|
||||
assert_receive {:connect_proxy, ^expected, "127.0.0.1", _port}
|
||||
end
|
||||
|
||||
test "uploads multipart bodies through an authenticated CONNECT tunnel", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
multipart = Tesla.Multipart.new() |> Tesla.Multipart.add_field("name", "value")
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200}} =
|
||||
request_multipart(origin.base_url <> "/upload", multipart, proxy_opts)
|
||||
|
||||
assert_receive {:origin_headers, "/upload", headers}
|
||||
assert String.downcase(headers) =~ "content-type: multipart/form-data;"
|
||||
assert_receive {:origin_body, "/upload", body}
|
||||
assert body =~ "name=\"name\""
|
||||
assert body =~ "value"
|
||||
end
|
||||
|
||||
test "uploads streamed bodies through an authenticated CONNECT tunnel", %{
|
||||
origin: origin,
|
||||
connect_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
body = Stream.map(["streamed", "-body"], & &1)
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200}} =
|
||||
request_stream(origin.base_url <> "/upload", body, proxy_opts)
|
||||
|
||||
assert_receive {:origin_body, "/upload", "streamed-body"}
|
||||
end
|
||||
|
||||
test "discards a partial HTTP/1 upload when body enumeration raises", %{origin: origin} do
|
||||
body =
|
||||
Stream.map(["partial", "raise"], fn
|
||||
"raise" -> raise "upload failed"
|
||||
part -> part
|
||||
end)
|
||||
|
||||
assert_raise RuntimeError, "upload failed", fn ->
|
||||
request_stream(origin.base_url <> "/upload", body, [])
|
||||
end
|
||||
|
||||
assert_receive {:origin_headers, "/upload", _headers}
|
||||
assert worker_protocol(origin.port) == nil
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(origin.base_url <> "/final")
|
||||
end
|
||||
|
||||
test "releases the lease when Gun rejects an invalid request header", %{origin: origin} do
|
||||
adapter_opts = [
|
||||
tls_opts: [verify: :verify_none],
|
||||
connect_timeout: 2_000,
|
||||
timeout: 2_000
|
||||
]
|
||||
|
||||
client = Tesla.client([ConnectionPool], Tesla.Adapter.Gun)
|
||||
|
||||
assert {:invalid_request_header, "x-test", _message} =
|
||||
catch_error(
|
||||
Tesla.get(client, origin.base_url <> "/final",
|
||||
headers: [{"x-test", "value\r\nx-injected: true"}],
|
||||
opts: [adapter: adapter_opts]
|
||||
)
|
||||
)
|
||||
|
||||
assert worker_clients(origin.port) == %{}
|
||||
refute_receive {:origin_request, "/final"}
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(origin.base_url <> "/final")
|
||||
end
|
||||
|
||||
test "requests through authenticated SOCKS5 with proxy-side DNS", %{
|
||||
origin: origin,
|
||||
socks_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [
|
||||
proxy: {:socks5, ~c"127.0.0.1", proxy.port},
|
||||
proxy_auth: {"alice", "secret"}
|
||||
]
|
||||
|
||||
url = "https://localhost:#{origin.port}/final"
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} = request(url, proxy_opts)
|
||||
|
||||
assert_receive {:socks5_proxy, "alice", "secret", "localhost", origin_port}
|
||||
assert origin_port == origin.port
|
||||
end
|
||||
|
||||
test "requests through unauthenticated SOCKS5 with an IP proxy host", %{
|
||||
origin: origin,
|
||||
socks_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [proxy: {:socks5, {127, 0, 0, 1}, proxy.port}]
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
|
||||
request(origin.base_url <> "/final", proxy_opts)
|
||||
|
||||
assert_receive {:socks5_proxy, nil, nil, "127.0.0.1", origin_port}
|
||||
assert origin_port == origin.port
|
||||
end
|
||||
|
||||
test "requests through unauthenticated SOCKS5 with a localhost proxy host", %{
|
||||
origin: origin,
|
||||
socks_proxy: proxy
|
||||
} do
|
||||
proxy_opts = [proxy: {:socks5, ~c"localhost", proxy.port}]
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
|
||||
request(origin.base_url <> "/final", proxy_opts)
|
||||
|
||||
assert_receive {:socks5_proxy, nil, nil, "127.0.0.1", origin_port}
|
||||
assert origin_port == origin.port
|
||||
end
|
||||
|
||||
defp request(url, proxy_opts \\ [], follow_redirects \\ false) do
|
||||
middleware =
|
||||
if follow_redirects do
|
||||
[Tesla.Middleware.FollowRedirects, ConnectionPool]
|
||||
else
|
||||
[ConnectionPool]
|
||||
end
|
||||
|
||||
adapter_opts =
|
||||
Keyword.merge(
|
||||
[tls_opts: [verify: :verify_none], connect_timeout: 2_000, timeout: 2_000],
|
||||
proxy_opts
|
||||
)
|
||||
|
||||
middleware
|
||||
|> Tesla.client(Tesla.Adapter.Gun)
|
||||
|> Tesla.get(url, opts: [adapter: adapter_opts])
|
||||
end
|
||||
|
||||
defp request_chunks(url, proxy_opts, follow_redirects \\ false) do
|
||||
adapter_opts =
|
||||
Keyword.merge(
|
||||
[
|
||||
body_as: :chunks,
|
||||
tls_opts: [verify: :verify_none],
|
||||
connect_timeout: 2_000,
|
||||
timeout: 2_000
|
||||
],
|
||||
proxy_opts
|
||||
)
|
||||
|
||||
middleware =
|
||||
if follow_redirects do
|
||||
[Tesla.Middleware.FollowRedirects, ConnectionPool]
|
||||
else
|
||||
[ConnectionPool]
|
||||
end
|
||||
|
||||
middleware
|
||||
|> Tesla.client(Tesla.Adapter.Gun)
|
||||
|> Tesla.get(url, opts: [adapter: adapter_opts])
|
||||
end
|
||||
|
||||
defp request_multipart(url, multipart, proxy_opts) do
|
||||
adapter_opts =
|
||||
Keyword.merge(
|
||||
[tls_opts: [verify: :verify_none], connect_timeout: 2_000, timeout: 2_000],
|
||||
proxy_opts
|
||||
)
|
||||
|
||||
[ConnectionPool]
|
||||
|> Tesla.client(Tesla.Adapter.Gun)
|
||||
|> Tesla.post(url, multipart, opts: [adapter: adapter_opts])
|
||||
end
|
||||
|
||||
defp request_stream(url, body, proxy_opts) do
|
||||
adapter_opts =
|
||||
Keyword.merge(
|
||||
[tls_opts: [verify: :verify_none], connect_timeout: 2_000, timeout: 2_000],
|
||||
proxy_opts
|
||||
)
|
||||
|
||||
[ConnectionPool]
|
||||
|> Tesla.client(Tesla.Adapter.Gun)
|
||||
|> Tesla.post(url, body, opts: [adapter: adapter_opts])
|
||||
end
|
||||
|
||||
defp collect_chunks(client, acc \\ "") do
|
||||
case Tesla.Adapter.Gun.read_chunk(client.pid, client.stream, client.opts) do
|
||||
{:nofin, chunk} -> collect_chunks(client, acc <> chunk)
|
||||
{:fin, chunk} -> acc <> chunk
|
||||
end
|
||||
end
|
||||
|
||||
defp start_tls_origin(parent) do
|
||||
certfile = Path.expand("../../fixtures/server.pem", __DIR__)
|
||||
keyfile = Path.expand("../../fixtures/private_key.pem", __DIR__)
|
||||
|
||||
{:ok, listener} =
|
||||
:ssl.listen(0, [
|
||||
:binary,
|
||||
certfile: certfile,
|
||||
keyfile: keyfile,
|
||||
reuseaddr: true,
|
||||
active: false,
|
||||
packet: :raw,
|
||||
ip: {127, 0, 0, 1}
|
||||
])
|
||||
|
||||
{:ok, {{127, 0, 0, 1}, port}} = :ssl.sockname(listener)
|
||||
{:ok, acceptor} = Task.start_link(fn -> accept_tls_loop(listener, parent) end)
|
||||
|
||||
%{
|
||||
listener: listener,
|
||||
acceptor: acceptor,
|
||||
transport: :ssl,
|
||||
port: port,
|
||||
base_url: "https://127.0.0.1:#{port}"
|
||||
}
|
||||
end
|
||||
|
||||
defp accept_tls_loop(listener, parent) do
|
||||
case :ssl.transport_accept(listener) do
|
||||
{:ok, tcp_socket} ->
|
||||
case :ssl.handshake(tcp_socket, 2_000) do
|
||||
{:ok, socket} ->
|
||||
pid = spawn(fn -> receive_and_serve_tls(socket, parent) end)
|
||||
:ok = :ssl.controlling_process(socket, pid)
|
||||
send(pid, :serve)
|
||||
|
||||
{:error, _reason} ->
|
||||
:gen_tcp.close(tcp_socket)
|
||||
end
|
||||
|
||||
accept_tls_loop(listener, parent)
|
||||
|
||||
{:error, _reason} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp receive_and_serve_tls(socket, parent) do
|
||||
receive do
|
||||
:serve -> serve_tls(socket, parent)
|
||||
end
|
||||
end
|
||||
|
||||
defp serve_tls(socket, parent) do
|
||||
case recv_headers(:ssl, socket) do
|
||||
{:ok, request} ->
|
||||
{headers, buffered_body} = split_headers(request)
|
||||
path = request_path(headers)
|
||||
send(parent, {:origin_request, path})
|
||||
send(parent, {:origin_headers, path, headers})
|
||||
|
||||
case path do
|
||||
"/redirect" ->
|
||||
send_tls_response(socket, 302, "Found", [{"Location", "/final"}], "")
|
||||
|
||||
"/redirect_chunks" ->
|
||||
send_tls_response(socket, 302, "Found", [{"Location", "/chunks"}], "discard-me")
|
||||
|
||||
"/final" ->
|
||||
send_tls_response(socket, 200, "OK", [], "ok")
|
||||
|
||||
"/chunks" ->
|
||||
send_tls_chunks(socket, ["hel", "lo"])
|
||||
|
||||
"/upload" ->
|
||||
{:ok, body} = recv_request_body(socket, headers, buffered_body)
|
||||
send(parent, {:origin_body, path, body})
|
||||
send_tls_response(socket, 200, "OK", [], "uploaded")
|
||||
|
||||
_ ->
|
||||
send_tls_response(socket, 404, "Not Found", [], "not found")
|
||||
end
|
||||
|
||||
serve_tls(socket, parent)
|
||||
|
||||
{:error, _reason} ->
|
||||
:ssl.close(socket)
|
||||
end
|
||||
end
|
||||
|
||||
defp send_tls_response(socket, status, reason, headers, body) do
|
||||
headers =
|
||||
[
|
||||
{"Content-Length", Integer.to_string(byte_size(body))},
|
||||
{"Connection", "keep-alive"}
|
||||
] ++ headers
|
||||
|
||||
:ssl.send(socket, [
|
||||
"HTTP/1.1 ",
|
||||
Integer.to_string(status),
|
||||
" ",
|
||||
reason,
|
||||
"\r\n",
|
||||
Enum.map(headers, fn {key, value} -> [key, ": ", value, "\r\n"] end),
|
||||
"\r\n",
|
||||
body
|
||||
])
|
||||
end
|
||||
|
||||
defp send_tls_chunks(socket, chunks) do
|
||||
:ssl.send(socket, [
|
||||
"HTTP/1.1 200 OK\r\n",
|
||||
"Transfer-Encoding: chunked\r\n",
|
||||
"Connection: keep-alive\r\n\r\n",
|
||||
Enum.map(chunks, fn chunk ->
|
||||
[Integer.to_string(byte_size(chunk), 16), "\r\n", chunk, "\r\n"]
|
||||
end),
|
||||
"0\r\n\r\n"
|
||||
])
|
||||
end
|
||||
|
||||
defp start_connect_proxy(parent) do
|
||||
start_tcp_server(fn socket -> serve_connect_proxy(socket, parent) end)
|
||||
end
|
||||
|
||||
defp serve_connect_proxy(socket, parent) do
|
||||
with {:ok, headers} <- recv_headers(:gen_tcp, socket),
|
||||
{:ok, host, port} <- parse_connect(headers),
|
||||
auth when is_binary(auth) <- header(headers, "proxy-authorization"),
|
||||
{:ok, upstream} <- tcp_connect(host, port) do
|
||||
send(parent, {:connect_proxy, auth, host, port})
|
||||
:ok = :gen_tcp.send(socket, "HTTP/1.1 200 Connection established\r\n\r\n")
|
||||
tunnel(socket, upstream)
|
||||
else
|
||||
_ ->
|
||||
:gen_tcp.send(
|
||||
socket,
|
||||
"HTTP/1.1 407 Proxy Authentication Required\r\nContent-Length: 0\r\n\r\n"
|
||||
)
|
||||
|
||||
:gen_tcp.close(socket)
|
||||
end
|
||||
end
|
||||
|
||||
defp start_forward_proxy(parent) do
|
||||
start_tcp_server(fn socket -> serve_forward_proxy(socket, parent) end)
|
||||
end
|
||||
|
||||
defp serve_forward_proxy(socket, parent) do
|
||||
with {:ok, request} <- recv_headers(:gen_tcp, socket) do
|
||||
{headers, _buffered_body} = split_headers(request)
|
||||
target = request_path(headers)
|
||||
host = header(headers, "host")
|
||||
auth = header(headers, "proxy-authorization")
|
||||
|
||||
if String.starts_with?(target, "http://") do
|
||||
send(parent, {:forward_proxy, target, host, auth})
|
||||
|
||||
:gen_tcp.send(
|
||||
socket,
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: keep-alive\r\n\r\nok"
|
||||
)
|
||||
|
||||
serve_forward_proxy(socket, parent)
|
||||
else
|
||||
:gen_tcp.send(
|
||||
socket,
|
||||
"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
||||
)
|
||||
|
||||
:gen_tcp.close(socket)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp start_socks5_proxy(parent) do
|
||||
start_tcp_server(fn socket -> serve_socks5_proxy(socket, parent) end)
|
||||
end
|
||||
|
||||
defp serve_socks5_proxy(socket, parent) do
|
||||
with {:ok, <<5, method_count>>} <- :gen_tcp.recv(socket, 2, 2_000),
|
||||
{:ok, methods} <- :gen_tcp.recv(socket, method_count, 2_000),
|
||||
{:ok, username, password} <- negotiate_socks5_auth(socket, methods),
|
||||
{:ok, host, port} <- recv_socks5_destination(socket),
|
||||
{:ok, upstream} <- tcp_connect(host, port),
|
||||
:ok <- :gen_tcp.send(socket, <<5, 0, 0, 1, 0, 0, 0, 0, 0, 0>>) do
|
||||
send(parent, {:socks5_proxy, username, password, host, port})
|
||||
tunnel(socket, upstream)
|
||||
else
|
||||
_ ->
|
||||
:gen_tcp.close(socket)
|
||||
end
|
||||
end
|
||||
|
||||
defp negotiate_socks5_auth(socket, methods) do
|
||||
cond do
|
||||
:binary.match(methods, <<2>>) != :nomatch ->
|
||||
with :ok <- :gen_tcp.send(socket, <<5, 2>>),
|
||||
{:ok, username, password} <- recv_socks5_auth(socket),
|
||||
:ok <- :gen_tcp.send(socket, <<1, 0>>) do
|
||||
{:ok, username, password}
|
||||
end
|
||||
|
||||
:binary.match(methods, <<0>>) != :nomatch ->
|
||||
with :ok <- :gen_tcp.send(socket, <<5, 0>>) do
|
||||
{:ok, nil, nil}
|
||||
end
|
||||
|
||||
true ->
|
||||
{:error, :unsupported_auth}
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_socks5_auth(socket) do
|
||||
with {:ok, <<1, username_length>>} <- :gen_tcp.recv(socket, 2, 2_000),
|
||||
{:ok, username} <- :gen_tcp.recv(socket, username_length, 2_000),
|
||||
{:ok, <<password_length>>} <- :gen_tcp.recv(socket, 1, 2_000),
|
||||
{:ok, password} <- :gen_tcp.recv(socket, password_length, 2_000) do
|
||||
{:ok, username, password}
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_socks5_destination(socket) do
|
||||
with {:ok, <<5, 1, 0, address_type>>} <- :gen_tcp.recv(socket, 4, 2_000),
|
||||
{:ok, host} <- recv_socks5_host(socket, address_type),
|
||||
{:ok, <<port::16>>} <- :gen_tcp.recv(socket, 2, 2_000) do
|
||||
{:ok, host, port}
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_socks5_host(socket, 1) do
|
||||
with {:ok, <<a, b, c, d>>} <- :gen_tcp.recv(socket, 4, 2_000) do
|
||||
{:ok, Enum.join([a, b, c, d], ".")}
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_socks5_host(socket, 3) do
|
||||
with {:ok, <<length>>} <- :gen_tcp.recv(socket, 1, 2_000),
|
||||
{:ok, host} <- :gen_tcp.recv(socket, length, 2_000) do
|
||||
{:ok, host}
|
||||
end
|
||||
end
|
||||
|
||||
defp start_tcp_server(handler) do
|
||||
{:ok, listener} =
|
||||
:gen_tcp.listen(0, [
|
||||
:binary,
|
||||
active: false,
|
||||
packet: :raw,
|
||||
reuseaddr: true,
|
||||
ip: {127, 0, 0, 1}
|
||||
])
|
||||
|
||||
{:ok, {{127, 0, 0, 1}, port}} = :inet.sockname(listener)
|
||||
{:ok, acceptor} = Task.start_link(fn -> accept_tcp_loop(listener, handler) end)
|
||||
%{listener: listener, acceptor: acceptor, transport: :gen_tcp, port: port}
|
||||
end
|
||||
|
||||
defp accept_tcp_loop(listener, handler) do
|
||||
case :gen_tcp.accept(listener) do
|
||||
{:ok, socket} ->
|
||||
pid = spawn(fn -> receive_and_serve_tcp(socket, handler) end)
|
||||
:ok = :gen_tcp.controlling_process(socket, pid)
|
||||
send(pid, :serve)
|
||||
accept_tcp_loop(listener, handler)
|
||||
|
||||
{:error, _reason} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp receive_and_serve_tcp(socket, handler) do
|
||||
receive do
|
||||
:serve -> handler.(socket)
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_headers(transport, socket, acc \\ <<>>) do
|
||||
case transport.recv(socket, 0, 2_000) do
|
||||
{:ok, data} ->
|
||||
acc = acc <> data
|
||||
|
||||
if :binary.match(acc, "\r\n\r\n") == :nomatch do
|
||||
recv_headers(transport, socket, acc)
|
||||
else
|
||||
{:ok, acc}
|
||||
end
|
||||
|
||||
{:error, _reason} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp request_path(headers) do
|
||||
headers
|
||||
|> String.split("\r\n", parts: 2)
|
||||
|> hd()
|
||||
|> String.split(" ")
|
||||
|> Enum.at(1)
|
||||
end
|
||||
|
||||
defp split_headers(request) do
|
||||
{index, 4} = :binary.match(request, "\r\n\r\n")
|
||||
split_at = index + 4
|
||||
<<headers::binary-size(split_at), body::binary>> = request
|
||||
{headers, body}
|
||||
end
|
||||
|
||||
defp recv_request_body(socket, headers, buffered_body) do
|
||||
cond do
|
||||
String.contains?(String.downcase(headers), "transfer-encoding: chunked") ->
|
||||
recv_chunked_body(socket, buffered_body, "")
|
||||
|
||||
content_length = header(headers, "content-length") ->
|
||||
recv_exact(socket, buffered_body, String.to_integer(content_length))
|
||||
|
||||
true ->
|
||||
{:ok, buffered_body}
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_chunked_body(socket, buffer, acc) do
|
||||
with {:ok, line, buffer} <- recv_line(socket, buffer),
|
||||
{size, ""} <- Integer.parse(line, 16) do
|
||||
if size == 0 do
|
||||
{:ok, acc}
|
||||
else
|
||||
with {:ok, chunk_and_crlf, buffer} <- take_bytes(socket, buffer, size + 2),
|
||||
<<chunk::binary-size(size), "\r\n">> <- chunk_and_crlf do
|
||||
recv_chunked_body(socket, buffer, acc <> chunk)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_line(socket, buffer) do
|
||||
case :binary.match(buffer, "\r\n") do
|
||||
{index, 2} ->
|
||||
<<line::binary-size(index), "\r\n", rest::binary>> = buffer
|
||||
{:ok, line, rest}
|
||||
|
||||
:nomatch ->
|
||||
with {:ok, data} <- :ssl.recv(socket, 0, 2_000) do
|
||||
recv_line(socket, buffer <> data)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp take_bytes(_socket, buffer, size) when byte_size(buffer) >= size do
|
||||
<<bytes::binary-size(size), rest::binary>> = buffer
|
||||
{:ok, bytes, rest}
|
||||
end
|
||||
|
||||
defp take_bytes(socket, buffer, size) do
|
||||
with {:ok, data} <- :ssl.recv(socket, 0, 2_000) do
|
||||
take_bytes(socket, buffer <> data, size)
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_exact(socket, buffer, size) do
|
||||
with {:ok, body, _rest} <- take_bytes(socket, buffer, size) do
|
||||
{:ok, body}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_connect(headers) do
|
||||
case headers |> String.split("\r\n", parts: 2) |> hd() |> String.split(" ") do
|
||||
["CONNECT", authority, _protocol] ->
|
||||
case String.split(authority, ":", parts: 2) do
|
||||
[host, port] -> {:ok, host, String.to_integer(port)}
|
||||
_ -> {:error, :invalid_authority}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, :invalid_connect}
|
||||
end
|
||||
end
|
||||
|
||||
defp header(headers, wanted_name) do
|
||||
headers
|
||||
|> String.split("\r\n")
|
||||
|> Enum.find_value(fn line ->
|
||||
case String.split(line, ":", parts: 2) do
|
||||
[name, value] ->
|
||||
if String.downcase(name) == wanted_name, do: String.trim(value)
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp tcp_connect(host, port) do
|
||||
:gen_tcp.connect(
|
||||
String.to_charlist(host),
|
||||
port,
|
||||
[:binary, active: false, packet: :raw],
|
||||
2_000
|
||||
)
|
||||
end
|
||||
|
||||
defp worker_protocol(port) do
|
||||
case worker_state(port) do
|
||||
nil -> nil
|
||||
state -> state.protocol
|
||||
end
|
||||
end
|
||||
|
||||
defp worker_clients(port) do
|
||||
case worker_state(port) do
|
||||
nil -> nil
|
||||
state -> state.clients
|
||||
end
|
||||
end
|
||||
|
||||
defp worker_state(port) do
|
||||
prefix = "https:127.0.0.1:#{port}:"
|
||||
|
||||
Pleroma.Gun.ConnectionPool
|
||||
|> Registry.select([{{:"$1", :"$2", :_}, [], [{{:"$1", :"$2"}}]}])
|
||||
|> Enum.find_value(fn {key, worker} ->
|
||||
if String.starts_with?(key, prefix), do: :sys.get_state(worker)
|
||||
end)
|
||||
end
|
||||
|
||||
defp tunnel(client, upstream) do
|
||||
:ok = :inet.setopts(client, active: :once)
|
||||
:ok = :inet.setopts(upstream, active: :once)
|
||||
tunnel_loop(client, upstream)
|
||||
end
|
||||
|
||||
defp tunnel_loop(client, upstream) do
|
||||
receive do
|
||||
{:tcp, ^client, data} ->
|
||||
:ok = :gen_tcp.send(upstream, data)
|
||||
:ok = :inet.setopts(client, active: :once)
|
||||
tunnel_loop(client, upstream)
|
||||
|
||||
{:tcp, ^upstream, data} ->
|
||||
:ok = :gen_tcp.send(client, data)
|
||||
:ok = :inet.setopts(upstream, active: :once)
|
||||
tunnel_loop(client, upstream)
|
||||
|
||||
{:tcp_closed, _socket} ->
|
||||
:gen_tcp.close(client)
|
||||
:gen_tcp.close(upstream)
|
||||
|
||||
{:tcp_error, _socket, _reason} ->
|
||||
:gen_tcp.close(client)
|
||||
:gen_tcp.close(upstream)
|
||||
after
|
||||
10_000 ->
|
||||
:gen_tcp.close(client)
|
||||
:gen_tcp.close(upstream)
|
||||
end
|
||||
end
|
||||
|
||||
defp stop_server(%{listener: listener, acceptor: acceptor, transport: transport}) do
|
||||
transport.close(listener)
|
||||
|
||||
if Process.alive?(acceptor), do: Process.exit(acceptor, :normal)
|
||||
end
|
||||
end
|
||||
404
test/pleroma/http/hackney_follow_redirect_regression_test.exs
Normal file
404
test/pleroma/http/hackney_follow_redirect_regression_test.exs
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.HackneyFollowRedirectRegressionTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
setup do
|
||||
{:ok, _} = Application.ensure_all_started(:hackney)
|
||||
|
||||
{:ok, tls_server} = start_tls_redirect_server()
|
||||
{:ok, proxy} = start_connect_proxy()
|
||||
|
||||
on_exit(fn ->
|
||||
stop_connect_proxy(proxy)
|
||||
stop_tls_redirect_server(tls_server)
|
||||
end)
|
||||
|
||||
{:ok, tls_server: tls_server, proxy: proxy}
|
||||
end
|
||||
|
||||
test "hackney follow_redirect crashes behind CONNECT proxy on relative redirects", %{
|
||||
tls_server: tls_server,
|
||||
proxy: proxy
|
||||
} do
|
||||
url = "#{tls_server.base_url}/redirect"
|
||||
|
||||
opts = [
|
||||
pool: :media,
|
||||
proxy: proxy.proxy_url,
|
||||
insecure: true,
|
||||
connect_timeout: 1_000,
|
||||
recv_timeout: 1_000,
|
||||
follow_redirect: true,
|
||||
force_redirect: true
|
||||
]
|
||||
|
||||
{pid, ref} = spawn_monitor(fn -> :hackney.request(:get, url, [], <<>>, opts) end)
|
||||
|
||||
assert_receive {:DOWN, ^ref, :process, ^pid, reason}, 5_000
|
||||
|
||||
assert match?({%FunctionClauseError{}, _}, reason) or match?(%FunctionClauseError{}, reason) or
|
||||
match?({:function_clause, _}, reason)
|
||||
end
|
||||
|
||||
test "redirects work via proxy when hackney follow_redirect is disabled", %{
|
||||
tls_server: tls_server,
|
||||
proxy: proxy
|
||||
} do
|
||||
url = "#{tls_server.base_url}/redirect"
|
||||
|
||||
adapter_opts = [
|
||||
pool: :media,
|
||||
proxy: proxy.proxy_url,
|
||||
insecure: true,
|
||||
connect_timeout: 1_000,
|
||||
recv_timeout: 1_000,
|
||||
follow_redirect: false,
|
||||
force_redirect: false,
|
||||
with_body: true
|
||||
]
|
||||
|
||||
client = Tesla.client([Tesla.Middleware.FollowRedirects], Tesla.Adapter.Hackney)
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
|
||||
Tesla.request(client, method: :get, url: url, opts: [adapter: adapter_opts])
|
||||
end
|
||||
|
||||
test "reverse proxy hackney client follows redirects via proxy without crashing", %{
|
||||
tls_server: tls_server,
|
||||
proxy: proxy
|
||||
} do
|
||||
url = "#{tls_server.base_url}/redirect"
|
||||
|
||||
opts = [
|
||||
pool: :media,
|
||||
proxy: proxy.proxy_url,
|
||||
insecure: true,
|
||||
connect_timeout: 1_000,
|
||||
recv_timeout: 1_000,
|
||||
follow_redirect: true
|
||||
]
|
||||
|
||||
assert {:ok, 200, _headers, ref} =
|
||||
Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts)
|
||||
|
||||
assert collect_body(ref) == "ok"
|
||||
Pleroma.ReverseProxy.Client.Hackney.close(ref)
|
||||
end
|
||||
|
||||
test "hackney reverse proxy follows nested redirects via proxy", %{
|
||||
tls_server: tls_server,
|
||||
proxy: proxy
|
||||
} do
|
||||
url = "#{tls_server.base_url}/nested_redirect"
|
||||
|
||||
opts = [
|
||||
pool: :media,
|
||||
proxy: proxy.proxy_url,
|
||||
insecure: true,
|
||||
connect_timeout: 1_000,
|
||||
recv_timeout: 1_000,
|
||||
follow_redirect: true
|
||||
]
|
||||
|
||||
assert {:ok, 200, _headers, ref} =
|
||||
Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts)
|
||||
|
||||
assert collect_body(ref) == "ok"
|
||||
Pleroma.ReverseProxy.Client.Hackney.close(ref)
|
||||
end
|
||||
|
||||
test "hackney reverse proxy stops following redirects after limit is reached", %{
|
||||
tls_server: tls_server,
|
||||
proxy: proxy
|
||||
} do
|
||||
url = "#{tls_server.base_url}/infinite_redirect"
|
||||
|
||||
opts = [
|
||||
pool: :media,
|
||||
proxy: proxy.proxy_url,
|
||||
insecure: true,
|
||||
connect_timeout: 1_000,
|
||||
recv_timeout: 1_000,
|
||||
follow_redirect: true
|
||||
]
|
||||
|
||||
assert {:ok, 302, _headers, ref} =
|
||||
Pleroma.ReverseProxy.Client.Hackney.request(:get, url, [], "", opts)
|
||||
|
||||
Pleroma.ReverseProxy.Client.Hackney.close(ref)
|
||||
end
|
||||
|
||||
defp collect_body(ref, acc \\ "") do
|
||||
case Pleroma.ReverseProxy.Client.Hackney.stream_body(ref) do
|
||||
:done -> acc
|
||||
{:ok, data, _ref} -> collect_body(ref, acc <> data)
|
||||
{:error, error} -> flunk("stream_body failed: #{inspect(error)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp start_tls_redirect_server do
|
||||
certfile = Path.expand("../../fixtures/server.pem", __DIR__)
|
||||
keyfile = Path.expand("../../fixtures/private_key.pem", __DIR__)
|
||||
|
||||
{:ok, listener} =
|
||||
:ssl.listen(0, [
|
||||
:binary,
|
||||
certfile: certfile,
|
||||
keyfile: keyfile,
|
||||
reuseaddr: true,
|
||||
active: false,
|
||||
packet: :raw,
|
||||
ip: {127, 0, 0, 1}
|
||||
])
|
||||
|
||||
{:ok, {{127, 0, 0, 1}, port}} = :ssl.sockname(listener)
|
||||
|
||||
{:ok, acceptor} =
|
||||
Task.start_link(fn ->
|
||||
accept_tls_loop(listener)
|
||||
end)
|
||||
|
||||
{:ok, %{listener: listener, acceptor: acceptor, base_url: "https://127.0.0.1:#{port}"}}
|
||||
end
|
||||
|
||||
defp stop_tls_redirect_server(%{listener: listener, acceptor: acceptor}) do
|
||||
:ok = :ssl.close(listener)
|
||||
|
||||
if Process.alive?(acceptor) do
|
||||
Process.exit(acceptor, :normal)
|
||||
end
|
||||
end
|
||||
|
||||
defp accept_tls_loop(listener) do
|
||||
case :ssl.transport_accept(listener) do
|
||||
{:ok, socket} ->
|
||||
_ = Task.start(fn -> serve_tls(socket) end)
|
||||
accept_tls_loop(listener)
|
||||
|
||||
{:error, :closed} ->
|
||||
:ok
|
||||
|
||||
{:error, _reason} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp serve_tls(tcp_socket) do
|
||||
with {:ok, ssl_socket} <- :ssl.handshake(tcp_socket, 2_000),
|
||||
{:ok, data} <- recv_ssl_headers(ssl_socket),
|
||||
{:ok, path} <- parse_path(data) do
|
||||
case path do
|
||||
"/infinite_redirect" ->
|
||||
send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/infinite_redirect"}], "")
|
||||
|
||||
"/nested_redirect" ->
|
||||
send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/redirect"}], "")
|
||||
|
||||
"/redirect" ->
|
||||
send_ssl_response(ssl_socket, 302, "Found", [{"Location", "/final"}], "")
|
||||
|
||||
"/final" ->
|
||||
send_ssl_response(ssl_socket, 200, "OK", [], "ok")
|
||||
|
||||
_ ->
|
||||
send_ssl_response(ssl_socket, 404, "Not Found", [], "not found")
|
||||
end
|
||||
|
||||
:ssl.close(ssl_socket)
|
||||
else
|
||||
_ ->
|
||||
_ = :gen_tcp.close(tcp_socket)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_ssl_headers(socket, acc \\ <<>>) do
|
||||
case :ssl.recv(socket, 0, 1_000) do
|
||||
{:ok, data} ->
|
||||
acc = acc <> data
|
||||
|
||||
if :binary.match(acc, "\r\n\r\n") != :nomatch do
|
||||
{:ok, acc}
|
||||
else
|
||||
if byte_size(acc) > 8_192 do
|
||||
{:error, :too_large}
|
||||
else
|
||||
recv_ssl_headers(socket, acc)
|
||||
end
|
||||
end
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp send_ssl_response(socket, status, reason, headers, body) do
|
||||
base_headers =
|
||||
[
|
||||
{"Content-Length", Integer.to_string(byte_size(body))},
|
||||
{"Connection", "close"}
|
||||
] ++ headers
|
||||
|
||||
iodata =
|
||||
[
|
||||
"HTTP/1.1 ",
|
||||
Integer.to_string(status),
|
||||
" ",
|
||||
reason,
|
||||
"\r\n",
|
||||
Enum.map(base_headers, fn {k, v} -> [k, ": ", v, "\r\n"] end),
|
||||
"\r\n",
|
||||
body
|
||||
]
|
||||
|
||||
:ssl.send(socket, iodata)
|
||||
end
|
||||
|
||||
defp start_connect_proxy do
|
||||
{:ok, listener} =
|
||||
:gen_tcp.listen(0, [
|
||||
:binary,
|
||||
active: false,
|
||||
packet: :raw,
|
||||
reuseaddr: true,
|
||||
ip: {127, 0, 0, 1}
|
||||
])
|
||||
|
||||
{:ok, {{127, 0, 0, 1}, port}} = :inet.sockname(listener)
|
||||
|
||||
{:ok, acceptor} =
|
||||
Task.start_link(fn ->
|
||||
accept_proxy_loop(listener)
|
||||
end)
|
||||
|
||||
{:ok, %{listener: listener, acceptor: acceptor, proxy_url: "127.0.0.1:#{port}"}}
|
||||
end
|
||||
|
||||
defp stop_connect_proxy(%{listener: listener, acceptor: acceptor}) do
|
||||
:ok = :gen_tcp.close(listener)
|
||||
|
||||
if Process.alive?(acceptor) do
|
||||
Process.exit(acceptor, :normal)
|
||||
end
|
||||
end
|
||||
|
||||
defp accept_proxy_loop(listener) do
|
||||
case :gen_tcp.accept(listener) do
|
||||
{:ok, socket} ->
|
||||
_ = Task.start(fn -> serve_proxy(socket) end)
|
||||
accept_proxy_loop(listener)
|
||||
|
||||
{:error, :closed} ->
|
||||
:ok
|
||||
|
||||
{:error, _reason} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp serve_proxy(client_socket) do
|
||||
with {:ok, {headers, rest}} <- recv_tcp_headers(client_socket),
|
||||
{:ok, {host, port}} <- parse_connect(headers),
|
||||
{:ok, upstream_socket} <- connect_upstream(host, port) do
|
||||
:gen_tcp.send(client_socket, "HTTP/1.1 200 Connection established\r\n\r\n")
|
||||
|
||||
if rest != <<>> do
|
||||
:gen_tcp.send(upstream_socket, rest)
|
||||
end
|
||||
|
||||
tunnel(client_socket, upstream_socket)
|
||||
else
|
||||
_ ->
|
||||
:gen_tcp.close(client_socket)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp tunnel(client_socket, upstream_socket) do
|
||||
parent = self()
|
||||
_ = spawn_link(fn -> forward(client_socket, upstream_socket, parent) end)
|
||||
_ = spawn_link(fn -> forward(upstream_socket, client_socket, parent) end)
|
||||
|
||||
receive do
|
||||
:tunnel_closed -> :ok
|
||||
after
|
||||
10_000 -> :ok
|
||||
end
|
||||
|
||||
:gen_tcp.close(client_socket)
|
||||
:gen_tcp.close(upstream_socket)
|
||||
end
|
||||
|
||||
defp forward(from_socket, to_socket, parent) do
|
||||
case :gen_tcp.recv(from_socket, 0, 10_000) do
|
||||
{:ok, data} ->
|
||||
_ = :gen_tcp.send(to_socket, data)
|
||||
forward(from_socket, to_socket, parent)
|
||||
|
||||
{:error, _reason} ->
|
||||
send(parent, :tunnel_closed)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp recv_tcp_headers(socket, acc \\ <<>>) do
|
||||
case :gen_tcp.recv(socket, 0, 1_000) do
|
||||
{:ok, data} ->
|
||||
acc = acc <> data
|
||||
|
||||
case :binary.match(acc, "\r\n\r\n") do
|
||||
:nomatch ->
|
||||
if byte_size(acc) > 8_192 do
|
||||
{:error, :too_large}
|
||||
else
|
||||
recv_tcp_headers(socket, acc)
|
||||
end
|
||||
|
||||
{idx, _len} ->
|
||||
split_at = idx + 4
|
||||
<<headers::binary-size(split_at), rest::binary>> = acc
|
||||
{:ok, {headers, rest}}
|
||||
end
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_connect(data) do
|
||||
with [request_line | _] <- String.split(data, "\r\n", trim: true),
|
||||
["CONNECT", hostport | _] <- String.split(request_line, " ", parts: 3),
|
||||
[host, port_str] <- String.split(hostport, ":", parts: 2),
|
||||
{port, ""} <- Integer.parse(port_str) do
|
||||
{:ok, {host, port}}
|
||||
else
|
||||
_ -> {:error, :invalid_connect}
|
||||
end
|
||||
end
|
||||
|
||||
defp connect_upstream(host, port) do
|
||||
address =
|
||||
case :inet.parse_address(String.to_charlist(host)) do
|
||||
{:ok, ip} -> ip
|
||||
{:error, _} -> String.to_charlist(host)
|
||||
end
|
||||
|
||||
:gen_tcp.connect(address, port, [:binary, active: false, packet: :raw], 1_000)
|
||||
end
|
||||
|
||||
defp parse_path(data) do
|
||||
case String.split(data, "\r\n", parts: 2) do
|
||||
[request_line | _] ->
|
||||
case String.split(request_line, " ") do
|
||||
[_method, path, _protocol] -> {:ok, path}
|
||||
_ -> {:error, :invalid_request}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, :invalid_request}
|
||||
end
|
||||
end
|
||||
end
|
||||
151
test/pleroma/http/hackney_redirect_regression_test.exs
Normal file
151
test/pleroma/http/hackney_redirect_regression_test.exs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.HackneyRedirectRegressionTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias Pleroma.HTTP.AdapterHelper.Hackney, as: HackneyAdapterHelper
|
||||
|
||||
setup do
|
||||
{:ok, _} = Application.ensure_all_started(:hackney)
|
||||
|
||||
{:ok, server} = start_server()
|
||||
on_exit(fn -> stop_server(server) end)
|
||||
|
||||
{:ok, server: server}
|
||||
end
|
||||
|
||||
test "pooled redirects work with follow_redirect disabled", %{server: server} do
|
||||
url = "#{server.base_url}/redirect"
|
||||
uri = URI.parse(url)
|
||||
|
||||
adapter_opts =
|
||||
HackneyAdapterHelper.options(
|
||||
[pool: :media, follow_redirect: false, no_proxy_env: true],
|
||||
uri
|
||||
)
|
||||
|
||||
client = Tesla.client([Tesla.Middleware.FollowRedirects], Tesla.Adapter.Hackney)
|
||||
|
||||
assert {:ok, %Tesla.Env{status: 200, body: "ok"}} =
|
||||
Tesla.request(client, method: :get, url: url, opts: [adapter: adapter_opts])
|
||||
end
|
||||
|
||||
defp start_server do
|
||||
{:ok, listener} =
|
||||
:gen_tcp.listen(0, [
|
||||
:binary,
|
||||
active: false,
|
||||
packet: :raw,
|
||||
reuseaddr: true,
|
||||
ip: {127, 0, 0, 1}
|
||||
])
|
||||
|
||||
{:ok, {{127, 0, 0, 1}, port}} = :inet.sockname(listener)
|
||||
|
||||
{:ok, acceptor} =
|
||||
Task.start_link(fn ->
|
||||
accept_loop(listener)
|
||||
end)
|
||||
|
||||
{:ok, %{listener: listener, acceptor: acceptor, base_url: "http://127.0.0.1:#{port}"}}
|
||||
end
|
||||
|
||||
defp stop_server(%{listener: listener, acceptor: acceptor}) do
|
||||
:ok = :gen_tcp.close(listener)
|
||||
|
||||
if Process.alive?(acceptor) do
|
||||
Process.exit(acceptor, :normal)
|
||||
end
|
||||
end
|
||||
|
||||
defp accept_loop(listener) do
|
||||
case :gen_tcp.accept(listener) do
|
||||
{:ok, socket} ->
|
||||
serve(socket)
|
||||
accept_loop(listener)
|
||||
|
||||
{:error, :closed} ->
|
||||
:ok
|
||||
|
||||
{:error, _reason} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp serve(socket) do
|
||||
with {:ok, data} <- recv_headers(socket),
|
||||
{:ok, path} <- parse_path(data) do
|
||||
case path do
|
||||
"/redirect" ->
|
||||
send_response(socket, 302, "Found", [{"Location", "/final"}], "")
|
||||
|
||||
"/final" ->
|
||||
send_response(socket, 200, "OK", [], "ok")
|
||||
|
||||
_ ->
|
||||
send_response(socket, 404, "Not Found", [], "not found")
|
||||
end
|
||||
else
|
||||
_ -> :ok
|
||||
end
|
||||
|
||||
:gen_tcp.close(socket)
|
||||
end
|
||||
|
||||
defp recv_headers(socket, acc \\ <<>>) do
|
||||
case :gen_tcp.recv(socket, 0, 1_000) do
|
||||
{:ok, data} ->
|
||||
acc = acc <> data
|
||||
|
||||
if :binary.match(acc, "\r\n\r\n") != :nomatch do
|
||||
{:ok, acc}
|
||||
else
|
||||
if byte_size(acc) > 8_192 do
|
||||
{:error, :too_large}
|
||||
else
|
||||
recv_headers(socket, acc)
|
||||
end
|
||||
end
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_path(data) do
|
||||
case String.split(data, "\r\n", parts: 2) do
|
||||
[request_line | _] ->
|
||||
case String.split(request_line, " ") do
|
||||
[_method, path, _protocol] -> {:ok, path}
|
||||
_ -> {:error, :invalid_request}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, :invalid_request}
|
||||
end
|
||||
end
|
||||
|
||||
defp send_response(socket, status, reason, headers, body) do
|
||||
base_headers =
|
||||
[
|
||||
{"Content-Length", Integer.to_string(byte_size(body))},
|
||||
{"Connection", "close"}
|
||||
] ++ headers
|
||||
|
||||
iodata =
|
||||
[
|
||||
"HTTP/1.1 ",
|
||||
Integer.to_string(status),
|
||||
" ",
|
||||
reason,
|
||||
"\r\n",
|
||||
Enum.map(base_headers, fn {k, v} -> [k, ": ", v, "\r\n"] end),
|
||||
"\r\n",
|
||||
body
|
||||
]
|
||||
|
||||
:gen_tcp.send(socket, iodata)
|
||||
end
|
||||
end
|
||||
|
|
@ -3,10 +3,13 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTPTest do
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnit.Case, async: false
|
||||
use Pleroma.Tests.Helpers
|
||||
|
||||
import Tesla.Mock
|
||||
|
||||
alias Pleroma.HTTP
|
||||
alias Pleroma.Utils.URIEncoding
|
||||
|
||||
setup do
|
||||
mock(fn
|
||||
|
|
@ -25,6 +28,39 @@ defmodule Pleroma.HTTPTest do
|
|||
|
||||
%{method: :post, url: "http://example.com/world"} ->
|
||||
%Tesla.Env{status: 200, body: "world"}
|
||||
|
||||
%{method: :get, url: "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz"} ->
|
||||
%Tesla.Env{status: 200, body: "emoji data"}
|
||||
|
||||
%{
|
||||
method: :get,
|
||||
url: "https://example.com/media/foo/bar%20!$&'()*+,;=/:%20@a%20%5Bbaz%5D.mp4"
|
||||
} ->
|
||||
%Tesla.Env{status: 200, body: "video data"}
|
||||
|
||||
%{method: :get, url: "https://example.com/media/unicode%20%F0%9F%99%82%20.gif"} ->
|
||||
%Tesla.Env{status: 200, body: "unicode data"}
|
||||
|
||||
%{
|
||||
method: :get,
|
||||
url:
|
||||
"https://i.guim.co.uk/img/media/1069ef13c447908272c4de94174cec2b6352cb2f/0_91_2000_1201/master/2000.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&precrop=40:21,offset-x50,offset-y0&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvdGctb3BpbmlvbnMtYWdlLTIwMTkucG5n&enable=upscale&s=cba21427a73512fdc9863c486c03fdd8"
|
||||
} ->
|
||||
%Tesla.Env{status: 200, body: "Guardian image quirk"}
|
||||
|
||||
%{
|
||||
method: :get,
|
||||
url:
|
||||
"https://i.guim.co.uk/emoji/Pack%201/koronebless.png?precrop=40:21,overlay-x0,overlay-y0&foo=bar+baz"
|
||||
} ->
|
||||
%Tesla.Env{status: 200, body: "Space in query with Guardian quirk"}
|
||||
|
||||
%{
|
||||
method: :get,
|
||||
url:
|
||||
"https://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=accessKEY%2F20130721%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130721T201207Z&X-Amz-Expires=86400&X-Amz-Signature=SIGNATURE&X-Amz-SignedHeaders=host"
|
||||
} ->
|
||||
%Tesla.Env{status: 200, body: "AWS S3 data"}
|
||||
end)
|
||||
|
||||
:ok
|
||||
|
|
@ -67,4 +103,115 @@ defmodule Pleroma.HTTPTest do
|
|||
}
|
||||
end
|
||||
end
|
||||
|
||||
test "URL encoding properly encodes URLs with spaces" do
|
||||
clear_config(:test_url_encoding, true)
|
||||
|
||||
url_with_space = "https://example.com/emoji/Pack 1/koronebless.png?foo=bar baz"
|
||||
|
||||
{:ok, result} = HTTP.get(url_with_space)
|
||||
|
||||
assert result.status == 200
|
||||
|
||||
properly_encoded_url = "https://example.com/emoji/Pack%201/koronebless.png?foo=bar+baz"
|
||||
|
||||
{:ok, result} = HTTP.get(properly_encoded_url)
|
||||
|
||||
assert result.status == 200
|
||||
|
||||
url_with_reserved_chars = "https://example.com/media/foo/bar !$&'()*+,;=/: @a [baz].mp4"
|
||||
|
||||
{:ok, result} = HTTP.get(url_with_reserved_chars)
|
||||
|
||||
assert result.status == 200
|
||||
|
||||
url_with_unicode = "https://example.com/media/unicode 🙂 .gif"
|
||||
|
||||
{:ok, result} = HTTP.get(url_with_unicode)
|
||||
|
||||
assert result.status == 200
|
||||
end
|
||||
|
||||
test "decodes URL first by default" do
|
||||
clear_config(:test_url_encoding, true)
|
||||
|
||||
normal_url = "https://example.com/media/file%20with%20space.jpg?name=a+space.jpg"
|
||||
|
||||
result = URIEncoding.encode_url(normal_url)
|
||||
|
||||
assert result == "https://example.com/media/file%20with%20space.jpg?name=a+space.jpg"
|
||||
end
|
||||
|
||||
test "doesn't decode URL first when specified" do
|
||||
clear_config(:test_url_encoding, true)
|
||||
|
||||
normal_url = "https://example.com/media/file%20with%20space.jpg"
|
||||
|
||||
result = URIEncoding.encode_url(normal_url, bypass_decode: true)
|
||||
|
||||
assert result == "https://example.com/media/file%2520with%2520space.jpg"
|
||||
end
|
||||
|
||||
test "properly applies Guardian image query quirk" do
|
||||
clear_config(:test_url_encoding, true)
|
||||
|
||||
url =
|
||||
"https://i.guim.co.uk/img/media/1069ef13c447908272c4de94174cec2b6352cb2f/0_91_2000_1201/master/2000.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&precrop=40:21,offset-x50,offset-y0&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvdGctb3BpbmlvbnMtYWdlLTIwMTkucG5n&enable=upscale&s=cba21427a73512fdc9863c486c03fdd8"
|
||||
|
||||
result = URIEncoding.encode_url(url)
|
||||
|
||||
assert result == url
|
||||
|
||||
{:ok, result_get} = HTTP.get(result)
|
||||
|
||||
assert result_get.status == 200
|
||||
end
|
||||
|
||||
test "properly encodes spaces as \"pluses\" in query when using quirks" do
|
||||
clear_config(:test_url_encoding, true)
|
||||
|
||||
url =
|
||||
"https://i.guim.co.uk/emoji/Pack 1/koronebless.png?precrop=40:21,overlay-x0,overlay-y0&foo=bar baz"
|
||||
|
||||
properly_encoded_url =
|
||||
"https://i.guim.co.uk/emoji/Pack%201/koronebless.png?precrop=40:21,overlay-x0,overlay-y0&foo=bar+baz"
|
||||
|
||||
result = URIEncoding.encode_url(url)
|
||||
|
||||
assert result == properly_encoded_url
|
||||
|
||||
{:ok, result_get} = HTTP.get(result)
|
||||
|
||||
assert result_get.status == 200
|
||||
end
|
||||
|
||||
test "properly encode AWS S3 queries" do
|
||||
clear_config(:test_url_encoding, true)
|
||||
|
||||
url =
|
||||
"https://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=accessKEY%2F20130721%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130721T201207Z&X-Amz-Expires=86400&X-Amz-Signature=SIGNATURE&X-Amz-SignedHeaders=host"
|
||||
|
||||
unencoded_url =
|
||||
"https://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=accessKEY/20130721/us-east-1/s3/aws4_request&X-Amz-Date=20130721T201207Z&X-Amz-Expires=86400&X-Amz-Signature=SIGNATURE&X-Amz-SignedHeaders=host"
|
||||
|
||||
result = URIEncoding.encode_url(url)
|
||||
result_unencoded = URIEncoding.encode_url(unencoded_url)
|
||||
|
||||
assert result == url
|
||||
assert result == result_unencoded
|
||||
|
||||
{:ok, result_get} = HTTP.get(result)
|
||||
|
||||
assert result_get.status == 200
|
||||
end
|
||||
|
||||
test "preserves query key order" do
|
||||
clear_config(:test_url_encoding, true)
|
||||
|
||||
url = "https://example.com/foo?hjkl=qwertz&xyz=abc&bar=baz"
|
||||
|
||||
result = URIEncoding.encode_url(url)
|
||||
|
||||
assert result == url
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,19 +3,15 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Instances.InstanceTest do
|
||||
alias Pleroma.Instances
|
||||
alias Pleroma.Instances.Instance
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Tests.ObanHelpers
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
use Oban.Testing, repo: Pleroma.Repo
|
||||
use Pleroma.DataCase
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
import Pleroma.Factory
|
||||
|
||||
setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1)
|
||||
|
||||
describe "set_reachable/1" do
|
||||
test "clears `unreachable_since` of existing matching Instance record having non-nil `unreachable_since`" do
|
||||
unreachable_since = NaiveDateTime.to_iso8601(NaiveDateTime.utc_now())
|
||||
|
|
@ -31,6 +27,32 @@ defmodule Pleroma.Instances.InstanceTest do
|
|||
assert {:ok, instance} = Instance.set_reachable(instance.host)
|
||||
refute instance.unreachable_since
|
||||
end
|
||||
|
||||
test "cancels all ReachabilityWorker jobs for the domain" do
|
||||
domain = "cancelme.example.org"
|
||||
insert(:instance, host: domain, unreachable_since: NaiveDateTime.utc_now())
|
||||
|
||||
# Insert a ReachabilityWorker job for this domain, scheduled 5 minutes in the future
|
||||
scheduled_at = DateTime.add(DateTime.utc_now(), 300, :second)
|
||||
|
||||
{:ok, job} =
|
||||
Pleroma.Workers.ReachabilityWorker.new(
|
||||
%{"domain" => domain, "phase" => "phase_1min", "attempt" => 1},
|
||||
scheduled_at: scheduled_at
|
||||
)
|
||||
|> Oban.insert()
|
||||
|
||||
# Ensure the job is present
|
||||
job = Pleroma.Repo.get(Oban.Job, job.id)
|
||||
assert job
|
||||
|
||||
# Call set_reachable, which should delete the job
|
||||
assert {:ok, _} = Instance.set_reachable(domain)
|
||||
|
||||
# Reload the job and assert it is deleted
|
||||
job = Pleroma.Repo.get(Oban.Job, job.id)
|
||||
refute job
|
||||
end
|
||||
end
|
||||
|
||||
describe "set_unreachable/1" do
|
||||
|
|
@ -145,7 +167,11 @@ defmodule Pleroma.Instances.InstanceTest do
|
|||
end
|
||||
|
||||
test "Doesn't scrapes unreachable instances" do
|
||||
instance = insert(:instance, unreachable_since: Instances.reachability_datetime_threshold())
|
||||
instance =
|
||||
insert(:instance,
|
||||
unreachable_since: NaiveDateTime.utc_now() |> NaiveDateTime.add(-:timer.hours(24))
|
||||
)
|
||||
|
||||
url = "https://" <> instance.host
|
||||
|
||||
assert capture_log(fn -> assert nil == Instance.get_or_update_favicon(URI.parse(url)) end) =~
|
||||
|
|
@ -213,32 +239,44 @@ defmodule Pleroma.Instances.InstanceTest do
|
|||
end
|
||||
end
|
||||
|
||||
test "delete_users_and_activities/1 deletes remote instance users and activities" do
|
||||
[mario, luigi, _peach, wario] =
|
||||
users = [
|
||||
insert(:user, nickname: "mario@mushroom.kingdom", name: "Mario"),
|
||||
insert(:user, nickname: "luigi@mushroom.kingdom", name: "Luigi"),
|
||||
insert(:user, nickname: "peach@mushroom.kingdom", name: "Peach"),
|
||||
insert(:user, nickname: "wario@greedville.biz", name: "Wario")
|
||||
]
|
||||
test "delete/1 schedules a job to delete the instance and users" do
|
||||
insert(:user, nickname: "mario@mushroom.kingdom", name: "Mario")
|
||||
|
||||
{:ok, post1} = CommonAPI.post(mario, %{status: "letsa go!"})
|
||||
{:ok, post2} = CommonAPI.post(luigi, %{status: "itsa me... luigi"})
|
||||
{:ok, post3} = CommonAPI.post(wario, %{status: "WHA-HA-HA!"})
|
||||
{:ok, _job} = Instance.delete("mushroom.kingdom")
|
||||
|
||||
{:ok, job} = Instance.delete_users_and_activities("mushroom.kingdom")
|
||||
:ok = ObanHelpers.perform(job)
|
||||
assert_enqueued(
|
||||
worker: Pleroma.Workers.DeleteWorker,
|
||||
args: %{"op" => "delete_instance", "host" => "mushroom.kingdom"}
|
||||
)
|
||||
end
|
||||
|
||||
[mario, luigi, peach, wario] = Repo.reload(users)
|
||||
describe "check_unreachable/1" do
|
||||
test "schedules a ReachabilityWorker job for the given domain" do
|
||||
domain = "test.example.com"
|
||||
|
||||
refute mario.is_active
|
||||
refute luigi.is_active
|
||||
refute peach.is_active
|
||||
refute peach.name == "Peach"
|
||||
# Call check_unreachable
|
||||
assert {:ok, _job} = Instance.check_unreachable(domain)
|
||||
|
||||
assert wario.is_active
|
||||
assert wario.name == "Wario"
|
||||
# Verify that a ReachabilityWorker job was scheduled
|
||||
jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker)
|
||||
assert length(jobs) == 1
|
||||
[job] = jobs
|
||||
assert job.args["domain"] == domain
|
||||
end
|
||||
|
||||
assert [nil, nil, %{}] = Repo.reload([post1, post2, post3])
|
||||
test "handles multiple calls for the same domain (uniqueness enforced)" do
|
||||
domain = "duplicate.example.com"
|
||||
|
||||
assert {:ok, _job1} = Instance.check_unreachable(domain)
|
||||
|
||||
# Second call for the same domain
|
||||
assert {:ok, %Oban.Job{conflict?: true}} = Instance.check_unreachable(domain)
|
||||
|
||||
# Should only have one job due to uniqueness
|
||||
jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker)
|
||||
assert length(jobs) == 1
|
||||
[job] = jobs
|
||||
assert job.args["domain"] == domain
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,74 +6,42 @@ defmodule Pleroma.InstancesTest do
|
|||
alias Pleroma.Instances
|
||||
|
||||
use Pleroma.DataCase
|
||||
|
||||
setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1)
|
||||
use Oban.Testing, repo: Pleroma.Repo
|
||||
|
||||
describe "reachable?/1" do
|
||||
test "returns `true` for host / url with unknown reachability status" do
|
||||
assert Instances.reachable?("unknown.site")
|
||||
assert Instances.reachable?("http://unknown.site")
|
||||
end
|
||||
|
||||
test "returns `false` for host / url marked unreachable for at least `reachability_datetime_threshold()`" do
|
||||
host = "consistently-unreachable.name"
|
||||
Instances.set_consistently_unreachable(host)
|
||||
|
||||
refute Instances.reachable?(host)
|
||||
refute Instances.reachable?("http://#{host}/path")
|
||||
end
|
||||
|
||||
test "returns `true` for host / url marked unreachable for less than `reachability_datetime_threshold()`" do
|
||||
url = "http://eventually-unreachable.name/path"
|
||||
|
||||
Instances.set_unreachable(url)
|
||||
|
||||
assert Instances.reachable?(url)
|
||||
assert Instances.reachable?(URI.parse(url).host)
|
||||
end
|
||||
|
||||
test "raises FunctionClauseError exception on non-binary input" do
|
||||
assert_raise FunctionClauseError, fn -> Instances.reachable?(nil) end
|
||||
assert_raise FunctionClauseError, fn -> Instances.reachable?(1) end
|
||||
end
|
||||
end
|
||||
|
||||
describe "filter_reachable/1" do
|
||||
setup do
|
||||
host = "consistently-unreachable.name"
|
||||
url1 = "http://eventually-unreachable.com/path"
|
||||
url2 = "http://domain.com/path"
|
||||
unreachable_host = "consistently-unreachable.name"
|
||||
reachable_host = "http://domain.com/path"
|
||||
|
||||
Instances.set_consistently_unreachable(host)
|
||||
Instances.set_unreachable(url1)
|
||||
Instances.set_unreachable(unreachable_host)
|
||||
|
||||
result = Instances.filter_reachable([host, url1, url2, nil])
|
||||
%{result: result, url1: url1, url2: url2}
|
||||
result = Instances.filter_reachable([unreachable_host, reachable_host, nil])
|
||||
%{result: result, reachable_host: reachable_host, unreachable_host: unreachable_host}
|
||||
end
|
||||
|
||||
test "returns a map with keys containing 'not marked consistently unreachable' elements of supplied list",
|
||||
%{result: result, url1: url1, url2: url2} do
|
||||
assert is_map(result)
|
||||
assert Enum.sort([url1, url2]) == result |> Map.keys() |> Enum.sort()
|
||||
test "returns a list of only reachable elements",
|
||||
%{result: result, reachable_host: reachable_host} do
|
||||
assert is_list(result)
|
||||
assert [reachable_host] == result
|
||||
end
|
||||
|
||||
test "returns a map with `unreachable_since` values for keys",
|
||||
%{result: result, url1: url1, url2: url2} do
|
||||
assert is_map(result)
|
||||
assert %NaiveDateTime{} = result[url1]
|
||||
assert is_nil(result[url2])
|
||||
end
|
||||
|
||||
test "returns an empty map for empty list or list containing no hosts / url" do
|
||||
assert %{} == Instances.filter_reachable([])
|
||||
assert %{} == Instances.filter_reachable([nil])
|
||||
test "returns an empty list when provided no data" do
|
||||
assert [] == Instances.filter_reachable([])
|
||||
assert [] == Instances.filter_reachable([nil])
|
||||
end
|
||||
end
|
||||
|
||||
describe "set_reachable/1" do
|
||||
test "sets unreachable url or host reachable" do
|
||||
host = "domain.com"
|
||||
Instances.set_consistently_unreachable(host)
|
||||
Instances.set_unreachable(host)
|
||||
refute Instances.reachable?(host)
|
||||
|
||||
Instances.set_reachable(host)
|
||||
|
|
@ -103,22 +71,68 @@ defmodule Pleroma.InstancesTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "set_consistently_unreachable/1" do
|
||||
test "sets reachable url or host unreachable" do
|
||||
url = "http://domain.com?q="
|
||||
assert Instances.reachable?(url)
|
||||
describe "check_all_unreachable/0" do
|
||||
test "schedules ReachabilityWorker jobs for all unreachable instances" do
|
||||
domain1 = "unreachable1.example.com"
|
||||
domain2 = "unreachable2.example.com"
|
||||
domain3 = "unreachable3.example.com"
|
||||
|
||||
Instances.set_consistently_unreachable(url)
|
||||
refute Instances.reachable?(url)
|
||||
Instances.set_unreachable(domain1)
|
||||
Instances.set_unreachable(domain2)
|
||||
Instances.set_unreachable(domain3)
|
||||
|
||||
Instances.check_all_unreachable()
|
||||
|
||||
# Verify that ReachabilityWorker jobs were scheduled for all unreachable domains
|
||||
jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker)
|
||||
assert length(jobs) == 3
|
||||
|
||||
domains = Enum.map(jobs, & &1.args["domain"])
|
||||
assert domain1 in domains
|
||||
assert domain2 in domains
|
||||
assert domain3 in domains
|
||||
end
|
||||
|
||||
test "keeps unreachable url or host unreachable" do
|
||||
host = "site.name"
|
||||
Instances.set_consistently_unreachable(host)
|
||||
refute Instances.reachable?(host)
|
||||
test "does not schedule jobs for reachable instances" do
|
||||
unreachable_domain = "unreachable.example.com"
|
||||
reachable_domain = "reachable.example.com"
|
||||
|
||||
Instances.set_consistently_unreachable(host)
|
||||
refute Instances.reachable?(host)
|
||||
Instances.set_unreachable(unreachable_domain)
|
||||
Instances.set_reachable(reachable_domain)
|
||||
|
||||
Instances.check_all_unreachable()
|
||||
|
||||
# Verify that only one job was scheduled (for the unreachable domain)
|
||||
jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker)
|
||||
assert length(jobs) == 1
|
||||
[job] = jobs
|
||||
assert job.args["domain"] == unreachable_domain
|
||||
end
|
||||
end
|
||||
|
||||
test "delete_all_unreachable/0 schedules DeleteWorker jobs for all unreachable instances" do
|
||||
domain1 = "unreachable1.example.com"
|
||||
domain2 = "unreachable2.example.com"
|
||||
domain3 = "unreachable3.example.com"
|
||||
|
||||
Instances.set_unreachable(domain1)
|
||||
Instances.set_unreachable(domain2)
|
||||
Instances.set_unreachable(domain3)
|
||||
|
||||
Instances.delete_all_unreachable()
|
||||
|
||||
# Verify that DeleteWorker jobs were scheduled for all unreachable domains
|
||||
jobs = all_enqueued(worker: Pleroma.Workers.DeleteWorker)
|
||||
assert length(jobs) == 3
|
||||
|
||||
domains = Enum.map(jobs, & &1.args["host"])
|
||||
assert domain1 in domains
|
||||
assert domain2 in domains
|
||||
assert domain3 in domains
|
||||
|
||||
# Verify all jobs are delete_instance operations
|
||||
Enum.each(jobs, fn job ->
|
||||
assert job.args["op"] == "delete_instance"
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|
|||
|
||||
alias Pleroma.Integration.WebsocketClient
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
alias Pleroma.Web.OAuth
|
||||
|
||||
@moduletag needs_streamer: true, capture_log: true
|
||||
|
|
@ -31,6 +32,48 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|
|||
WebsocketClient.start_link(self(), path, headers)
|
||||
end
|
||||
|
||||
defp raw_websocket_handshake(qs, headers) do
|
||||
uri = URI.parse(@path <> qs)
|
||||
port = uri.port || 80
|
||||
path = uri.path <> if(uri.query, do: "?" <> uri.query, else: "")
|
||||
|
||||
default_headers = [
|
||||
{"host", "#{uri.host}:#{port}"},
|
||||
{"upgrade", "websocket"},
|
||||
{"connection", "Upgrade"},
|
||||
{"sec-websocket-key", Base.encode64(:crypto.strong_rand_bytes(16))},
|
||||
{"sec-websocket-version", "13"}
|
||||
]
|
||||
|
||||
request = [
|
||||
"GET #{path} HTTP/1.1\r\n",
|
||||
Enum.map(default_headers ++ headers, fn {name, value} -> "#{name}: #{value}\r\n" end),
|
||||
"\r\n"
|
||||
]
|
||||
|
||||
with {:ok, socket} <-
|
||||
:gen_tcp.connect(String.to_charlist(uri.host), port, [:binary, active: false], 1_000),
|
||||
:ok <- :gen_tcp.send(socket, request),
|
||||
{:ok, response} <- :gen_tcp.recv(socket, 0, 1_000) do
|
||||
:gen_tcp.close(socket)
|
||||
{:ok, parse_http_response(response)}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_http_response(response) do
|
||||
[headers | _] = String.split(response, "\r\n\r\n", parts: 2)
|
||||
[status_line | header_lines] = String.split(headers, "\r\n")
|
||||
[_, status | _] = String.split(status_line, " ")
|
||||
|
||||
headers =
|
||||
Enum.map(header_lines, fn line ->
|
||||
[name, value] = String.split(line, ":", parts: 2)
|
||||
{String.downcase(name), String.trim(value)}
|
||||
end)
|
||||
|
||||
%{status: String.to_integer(status), headers: headers}
|
||||
end
|
||||
|
||||
defp decode_json(json) do
|
||||
with {:ok, %{"event" => event, "payload" => payload_text}} <- Jason.decode(json),
|
||||
{:ok, payload} <- Jason.decode(payload_text) do
|
||||
|
|
@ -85,9 +128,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|
|||
assert json["payload"]
|
||||
assert {:ok, json} = Jason.decode(json["payload"])
|
||||
|
||||
view_json =
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil)
|
||||
|> atom_key_to_string()
|
||||
view_json = atom_key_to_string(StatusView.render("show.json", activity: activity, for: nil))
|
||||
|
||||
assert json == view_json
|
||||
end
|
||||
|
|
@ -114,10 +155,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|
|||
assert json["payload"]
|
||||
assert {:ok, json} = Jason.decode(json["payload"])
|
||||
|
||||
view_json =
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil)
|
||||
|> Jason.encode!()
|
||||
|> Jason.decode!()
|
||||
view_json = atom_key_to_string(StatusView.render("show.json", activity: activity, for: nil))
|
||||
|
||||
assert json == view_json
|
||||
end
|
||||
|
|
@ -268,6 +306,65 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|
|||
end)
|
||||
end
|
||||
|
||||
test "accepts valid token on Sec-WebSocket-Protocol header", %{token: token} do
|
||||
assert {:ok, _} = start_socket("?stream=user", [{"Sec-WebSocket-Protocol", token.token}])
|
||||
|
||||
capture_log(fn ->
|
||||
assert {:error, %WebSockex.RequestError{code: 401}} =
|
||||
start_socket("?stream=user", [{"Sec-WebSocket-Protocol", "I am a friend"}])
|
||||
|
||||
Process.sleep(30)
|
||||
end)
|
||||
end
|
||||
|
||||
test "echoes the Sec-WebSocket-Protocol token in the handshake", %{token: token} do
|
||||
assert {:ok, %{status: 101, headers: headers}} =
|
||||
raw_websocket_handshake("?stream=user", [
|
||||
{"sec-websocket-protocol", token.token}
|
||||
])
|
||||
|
||||
assert {"sec-websocket-protocol", token.token} in headers
|
||||
end
|
||||
|
||||
test "echoes the selected Sec-WebSocket-Protocol token", %{token: token} do
|
||||
assert {:ok, %{status: 101, headers: headers}} =
|
||||
raw_websocket_handshake("?stream=user", [
|
||||
{"sec-websocket-protocol", "#{token.token}, phoenix"}
|
||||
])
|
||||
|
||||
assert {"sec-websocket-protocol", token.token} in headers
|
||||
end
|
||||
|
||||
test "does not echo an invalid Sec-WebSocket-Protocol token", %{token: token} do
|
||||
assert {:ok, %{status: 401, headers: headers}} =
|
||||
raw_websocket_handshake("?stream=user", [
|
||||
{"sec-websocket-protocol", "invalid"}
|
||||
])
|
||||
|
||||
refute {"sec-websocket-protocol", token.token} in headers
|
||||
refute List.keymember?(headers, "sec-websocket-protocol", 0)
|
||||
end
|
||||
|
||||
test "prefers sec-websocket-protocol token over query access_token", %{
|
||||
token: token,
|
||||
user: user
|
||||
} do
|
||||
assert {:ok, state} =
|
||||
Pleroma.Web.MastodonAPI.WebsocketHandler.connect(%{
|
||||
params: %{"stream" => "user", "access_token" => "invalid"},
|
||||
connect_info: %{
|
||||
sec_websocket_headers: [
|
||||
{"sec-websocket-version", "13"},
|
||||
{"sec-websocket-protocol", token.token}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
assert state.user.id == user.id
|
||||
assert state.oauth_token.id == token.id
|
||||
assert state.topics != []
|
||||
end
|
||||
|
||||
test "accepts valid token on client-sent event", %{token: token} do
|
||||
assert {:ok, pid} = start_socket()
|
||||
|
||||
|
|
@ -352,7 +449,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|
|||
test "accepts the 'list' stream", %{token: token, user: user} do
|
||||
posting_user = insert(:user)
|
||||
|
||||
{:ok, list} = Pleroma.List.create("test", user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "test"}, user)
|
||||
Pleroma.List.follow(list, posting_user)
|
||||
|
||||
assert {:ok, _} = start_socket("?stream=list&access_token=#{token.token}&list=#{list.id}")
|
||||
|
|
@ -404,7 +501,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|
|||
|
||||
test "receives private statuses", %{user: reading_user, token: token} do
|
||||
user = insert(:user)
|
||||
CommonAPI.follow(reading_user, user)
|
||||
CommonAPI.follow(user, reading_user)
|
||||
|
||||
{:ok, _} = start_socket("?stream=user&access_token=#{token.token}")
|
||||
|
||||
|
|
@ -419,19 +516,19 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|
|||
assert {:ok, json} = Jason.decode(json["payload"])
|
||||
|
||||
view_json =
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("show.json",
|
||||
activity: activity,
|
||||
for: reading_user
|
||||
atom_key_to_string(
|
||||
StatusView.render("show.json",
|
||||
activity: activity,
|
||||
for: reading_user
|
||||
)
|
||||
)
|
||||
|> Jason.encode!()
|
||||
|> Jason.decode!()
|
||||
|
||||
assert json == view_json
|
||||
end
|
||||
|
||||
test "receives edits", %{user: reading_user, token: token} do
|
||||
user = insert(:user)
|
||||
CommonAPI.follow(reading_user, user)
|
||||
CommonAPI.follow(user, reading_user)
|
||||
|
||||
{:ok, _} = start_socket("?stream=user&access_token=#{token.token}")
|
||||
|
||||
|
|
@ -440,26 +537,26 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|
|||
|
||||
assert_receive {:text, _raw_json}, 1_000
|
||||
|
||||
{:ok, _} = CommonAPI.update(user, activity, %{status: "mew mew", visibility: "private"})
|
||||
{:ok, _} = CommonAPI.update(activity, user, %{status: "mew mew", visibility: "private"})
|
||||
|
||||
assert_receive {:text, raw_json}, 1_000
|
||||
|
||||
activity = Pleroma.Activity.normalize(activity)
|
||||
|
||||
view_json =
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("show.json",
|
||||
activity: activity,
|
||||
for: reading_user
|
||||
atom_key_to_string(
|
||||
StatusView.render("show.json",
|
||||
activity: activity,
|
||||
for: reading_user
|
||||
)
|
||||
)
|
||||
|> Jason.encode!()
|
||||
|> Jason.decode!()
|
||||
|
||||
assert {:ok, %{"event" => "status.update", "payload" => ^view_json}} = decode_json(raw_json)
|
||||
end
|
||||
|
||||
test "receives notifications", %{user: reading_user, token: token} do
|
||||
user = insert(:user)
|
||||
CommonAPI.follow(reading_user, user)
|
||||
CommonAPI.follow(user, reading_user)
|
||||
|
||||
{:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}")
|
||||
|
||||
|
|
|
|||
|
|
@ -28,13 +28,13 @@ defmodule Pleroma.JobQueueMonitorTest do
|
|||
queue: "background",
|
||||
stack: [
|
||||
{Pleroma.Workers.BackgroundWorker, :perform, 2,
|
||||
[file: 'lib/pleroma/workers/background_worker.ex', line: 31]},
|
||||
[file: ~c"lib/pleroma/workers/background_worker.ex", line: 31]},
|
||||
{Oban.Queue.Executor, :safe_call, 1,
|
||||
[file: 'lib/oban/queue/executor.ex', line: 42]},
|
||||
{:timer, :tc, 3, [file: 'timer.erl', line: 197]},
|
||||
{Oban.Queue.Executor, :call, 2, [file: 'lib/oban/queue/executor.ex', line: 23]},
|
||||
{Task.Supervised, :invoke_mfa, 2, [file: 'lib/task/supervised.ex', line: 90]},
|
||||
{:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 249]}
|
||||
[file: ~c"lib/oban/queue/executor.ex", line: 42]},
|
||||
{:timer, :tc, 3, [file: ~c"timer.erl", line: 197]},
|
||||
{Oban.Queue.Executor, :call, 2, [file: ~c"lib/oban/queue/executor.ex", line: 23]},
|
||||
{Task.Supervised, :invoke_mfa, 2, [file: ~c"lib/task/supervised.ex", line: 90]},
|
||||
{:proc_lib, :init_p_do_apply, 3, [file: ~c"proc_lib.erl", line: 249]}
|
||||
],
|
||||
worker: "Pleroma.Workers.BackgroundWorker"
|
||||
}}
|
||||
|
|
|
|||
56
test/pleroma/language/language_detector_test.exs
Normal file
56
test/pleroma/language/language_detector_test.exs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Language.LanguageDetectorTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.Language.LanguageDetector
|
||||
alias Pleroma.Language.LanguageDetectorMock
|
||||
alias Pleroma.StaticStubbedConfigMock
|
||||
|
||||
import Mox
|
||||
|
||||
setup do
|
||||
# Stub the StaticStubbedConfigMock to return our mock for the provider
|
||||
StaticStubbedConfigMock
|
||||
|> stub(:get, fn
|
||||
[Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock
|
||||
_other -> nil
|
||||
end)
|
||||
|
||||
# Stub the LanguageDetectorMock with default implementations
|
||||
LanguageDetectorMock
|
||||
|> stub(:missing_dependencies, fn -> [] end)
|
||||
|> stub(:configured?, fn -> true end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "it detects text language" do
|
||||
LanguageDetectorMock
|
||||
|> expect(:detect, fn _text -> "fr" end)
|
||||
|
||||
detected_language = LanguageDetector.detect("Je viens d'atterrir en Tchéquie.")
|
||||
|
||||
assert detected_language == "fr"
|
||||
end
|
||||
|
||||
test "it returns nil if text is not long enough" do
|
||||
# No need to set expectations as the word count check happens before the provider is called
|
||||
|
||||
detected_language = LanguageDetector.detect("it returns nil")
|
||||
|
||||
assert detected_language == nil
|
||||
end
|
||||
|
||||
test "it returns nil if no provider specified" do
|
||||
# Override the stub to return nil for the provider
|
||||
StaticStubbedConfigMock
|
||||
|> expect(:get, fn [Pleroma.Language.LanguageDetector, :provider] -> nil end)
|
||||
|
||||
detected_language = LanguageDetector.detect("this should also return nil")
|
||||
|
||||
assert detected_language == nil
|
||||
end
|
||||
end
|
||||
37
test/pleroma/language/translation/deepl_test.exs
Normal file
37
test/pleroma/language/translation/deepl_test.exs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Language.Translation.DeeplTest do
|
||||
use Pleroma.Web.ConnCase
|
||||
|
||||
alias Pleroma.Language.Translation.Deepl
|
||||
|
||||
test "it translates text" do
|
||||
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||
clear_config([Pleroma.Language.Translation.Deepl, :base_url], "https://api-free.deepl.com")
|
||||
clear_config([Pleroma.Language.Translation.Deepl, :api_key], "API_KEY")
|
||||
|
||||
{:ok, res} =
|
||||
Deepl.translate(
|
||||
"USUNĄĆ ŚLEDZIKA!Wklej to na swojego śledzika. Jeżeli uzbieramy 70% użytkowników nk...to usuną śledzika!!!",
|
||||
"pl",
|
||||
"en"
|
||||
)
|
||||
|
||||
assert %{
|
||||
detected_source_language: "PL",
|
||||
provider: "DeepL"
|
||||
} = res
|
||||
end
|
||||
|
||||
test "it returns languages list" do
|
||||
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||
clear_config([Pleroma.Language.Translation.Deepl, :base_url], "https://api-free.deepl.com")
|
||||
clear_config([Pleroma.Language.Translation.Deepl, :api_key], "API_KEY")
|
||||
|
||||
assert {:ok, [language | _languages]} = Deepl.supported_languages(:target)
|
||||
|
||||
assert is_binary(language)
|
||||
end
|
||||
end
|
||||
59
test/pleroma/language/translation/translate_locally_test.exs
Normal file
59
test/pleroma/language/translation/translate_locally_test.exs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Language.Translation.TranslateLocallyTest do
|
||||
use Pleroma.DataCase
|
||||
|
||||
alias Pleroma.Language.Translation.TranslateLocally
|
||||
|
||||
@example_models %{
|
||||
"de" => %{
|
||||
"en" => "de-en-base"
|
||||
},
|
||||
"en" => %{
|
||||
"de" => "en-de-base",
|
||||
"pl" => "en-pl-tiny"
|
||||
},
|
||||
"cs" => %{
|
||||
"en" => "cs-en-base"
|
||||
},
|
||||
"pl" => %{
|
||||
"en" => "pl-en-tiny"
|
||||
}
|
||||
}
|
||||
|
||||
test "it returns languages list" do
|
||||
clear_config([Pleroma.Language.Translation.TranslateLocally, :models], @example_models)
|
||||
|
||||
assert {:ok, languages} = TranslateLocally.supported_languages(:source)
|
||||
assert ["cs", "de", "en", "pl"] = languages |> Enum.sort()
|
||||
end
|
||||
|
||||
describe "it returns languages matrix" do
|
||||
test "without intermediary language" do
|
||||
clear_config([Pleroma.Language.Translation.TranslateLocally, :models], @example_models)
|
||||
|
||||
assert {:ok,
|
||||
%{
|
||||
"cs" => ["en"],
|
||||
"de" => ["en"],
|
||||
"en" => ["de", "pl"],
|
||||
"pl" => ["en"]
|
||||
}} = TranslateLocally.languages_matrix()
|
||||
end
|
||||
|
||||
test "with intermediary language" do
|
||||
clear_config([Pleroma.Language.Translation.TranslateLocally, :models], @example_models)
|
||||
clear_config([Pleroma.Language.Translation.TranslateLocally, :intermediary_language], "en")
|
||||
|
||||
assert {:ok,
|
||||
%{
|
||||
"cs" => ["de", "en", "pl"],
|
||||
"de" => ["en", "pl"],
|
||||
"en" => ["de", "pl"],
|
||||
"pl" => ["de", "en"]
|
||||
}} = TranslateLocally.languages_matrix()
|
||||
end
|
||||
end
|
||||
end
|
||||
28
test/pleroma/language/translation_test.exs
Normal file
28
test/pleroma/language/translation_test.exs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
defmodule Pleroma.Language.TranslationTest do
|
||||
use Pleroma.Web.ConnCase
|
||||
|
||||
alias Pleroma.Language.Translation
|
||||
|
||||
setup do: clear_config([Pleroma.Language.Translation, :provider], TranslationMock)
|
||||
|
||||
test "it translates text" do
|
||||
assert {:ok,
|
||||
%{
|
||||
content: "txet emos",
|
||||
detected_source_language: _,
|
||||
provider: _
|
||||
}} = Translation.translate("some text", "en", "uk")
|
||||
end
|
||||
|
||||
test "it stores translation result in cache" do
|
||||
Translation.translate("some text", "en", "uk")
|
||||
|
||||
assert {:ok, result} =
|
||||
Cachex.get(
|
||||
:translations_cache,
|
||||
"en/uk/#{:crypto.hash(:sha256, "some text") |> Base.encode64()}"
|
||||
)
|
||||
|
||||
assert result.content == "txet emos"
|
||||
end
|
||||
end
|
||||
|
|
@ -10,22 +10,46 @@ defmodule Pleroma.ListTest do
|
|||
|
||||
test "creating a list" do
|
||||
user = insert(:user)
|
||||
{:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user)
|
||||
%Pleroma.List{title: title} = Pleroma.List.get(list.id, user)
|
||||
{:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title"}, user)
|
||||
%Pleroma.List{title: title, exclusive: exclusive} = Pleroma.List.get(list.id, user)
|
||||
assert title == "title"
|
||||
assert exclusive == false
|
||||
end
|
||||
|
||||
test "validates title" do
|
||||
user = insert(:user)
|
||||
|
||||
assert {:error, changeset} = Pleroma.List.create("", user)
|
||||
assert {:error, changeset} = Pleroma.List.create(%{title: ""}, user)
|
||||
assert changeset.errors == [title: {"can't be blank", [validation: :required]}]
|
||||
end
|
||||
|
||||
test "creating a list with an emoji" do
|
||||
user = insert(:user)
|
||||
{:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title", emoji: "🕓"}, user)
|
||||
%Pleroma.List{emoji: emoji} = Pleroma.List.get(list.id, user)
|
||||
assert emoji == "🕓"
|
||||
end
|
||||
|
||||
test "validates emoji" do
|
||||
user = insert(:user)
|
||||
|
||||
assert {:error, changeset} =
|
||||
Pleroma.List.create(%{title: "title", emoji: "not an emoji"}, user)
|
||||
|
||||
assert changeset.errors == [emoji: {"Invalid emoji", []}]
|
||||
end
|
||||
|
||||
test "updating a list's emoji" do
|
||||
user = insert(:user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "title"}, user)
|
||||
{:ok, %{emoji: emoji}} = Pleroma.List.update(list, %{emoji: "🕓"})
|
||||
assert "🕓" == emoji
|
||||
end
|
||||
|
||||
test "getting a list not belonging to the user" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
{:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user)
|
||||
{:ok, %Pleroma.List{} = list} = Pleroma.List.create(%{title: "title"}, user)
|
||||
ret = Pleroma.List.get(list.id, other_user)
|
||||
assert is_nil(ret)
|
||||
end
|
||||
|
|
@ -33,7 +57,7 @@ defmodule Pleroma.ListTest do
|
|||
test "adding an user to a list" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
{:ok, list} = Pleroma.List.create("title", user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "title"}, user)
|
||||
{:ok, %{following: following}} = Pleroma.List.follow(list, other_user)
|
||||
assert [other_user.follower_address] == following
|
||||
end
|
||||
|
|
@ -41,7 +65,7 @@ defmodule Pleroma.ListTest do
|
|||
test "removing an user from a list" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
{:ok, list} = Pleroma.List.create("title", user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "title"}, user)
|
||||
{:ok, %{following: _following}} = Pleroma.List.follow(list, other_user)
|
||||
{:ok, %{following: following}} = Pleroma.List.unfollow(list, other_user)
|
||||
assert [] == following
|
||||
|
|
@ -49,14 +73,27 @@ defmodule Pleroma.ListTest do
|
|||
|
||||
test "renaming a list" do
|
||||
user = insert(:user)
|
||||
{:ok, list} = Pleroma.List.create("title", user)
|
||||
{:ok, %{title: title}} = Pleroma.List.rename(list, "new")
|
||||
{:ok, list} = Pleroma.List.create(%{title: "title"}, user)
|
||||
{:ok, %{title: title}} = Pleroma.List.update(list, %{title: "new"})
|
||||
assert "new" == title
|
||||
end
|
||||
|
||||
test "updating a list exclusivity" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, %{exclusive: exclusive} = list} =
|
||||
Pleroma.List.create(%{title: "title", exclusive: true}, user)
|
||||
|
||||
assert exclusive == true
|
||||
{:ok, %{exclusive: exclusive} = list} = Pleroma.List.update(list, %{exclusive: false})
|
||||
assert exclusive == false
|
||||
{:ok, %{exclusive: exclusive}} = Pleroma.List.update(list, %{exclusive: true})
|
||||
assert exclusive == true
|
||||
end
|
||||
|
||||
test "deleting a list" do
|
||||
user = insert(:user)
|
||||
{:ok, list} = Pleroma.List.create("title", user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "title"}, user)
|
||||
{:ok, list} = Pleroma.List.delete(list)
|
||||
assert is_nil(Repo.get(Pleroma.List, list.id))
|
||||
end
|
||||
|
|
@ -65,7 +102,7 @@ defmodule Pleroma.ListTest do
|
|||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
third_user = insert(:user)
|
||||
{:ok, list} = Pleroma.List.create("title", user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "title"}, user)
|
||||
{:ok, list} = Pleroma.List.follow(list, other_user)
|
||||
{:ok, list} = Pleroma.List.follow(list, third_user)
|
||||
{:ok, following} = Pleroma.List.get_following(list)
|
||||
|
|
@ -76,9 +113,9 @@ defmodule Pleroma.ListTest do
|
|||
test "getting all lists by an user" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
{:ok, list_one} = Pleroma.List.create("title", user)
|
||||
{:ok, list_two} = Pleroma.List.create("other title", user)
|
||||
{:ok, list_three} = Pleroma.List.create("third title", other_user)
|
||||
{:ok, list_one} = Pleroma.List.create(%{title: "title"}, user)
|
||||
{:ok, list_two} = Pleroma.List.create(%{title: "other title"}, user)
|
||||
{:ok, list_three} = Pleroma.List.create(%{title: "third title"}, other_user)
|
||||
lists = Pleroma.List.for_user(user, %{})
|
||||
assert list_one in lists
|
||||
assert list_two in lists
|
||||
|
|
@ -88,9 +125,9 @@ defmodule Pleroma.ListTest do
|
|||
test "getting all lists the user is a member of" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
{:ok, list_one} = Pleroma.List.create("title", user)
|
||||
{:ok, list_two} = Pleroma.List.create("other title", user)
|
||||
{:ok, list_three} = Pleroma.List.create("third title", other_user)
|
||||
{:ok, list_one} = Pleroma.List.create(%{title: "title"}, user)
|
||||
{:ok, list_two} = Pleroma.List.create(%{title: "other title"}, user)
|
||||
{:ok, list_three} = Pleroma.List.create(%{title: "third title"}, other_user)
|
||||
{:ok, list_one} = Pleroma.List.follow(list_one, other_user)
|
||||
{:ok, list_two} = Pleroma.List.follow(list_two, other_user)
|
||||
{:ok, list_three} = Pleroma.List.follow(list_three, user)
|
||||
|
|
@ -106,8 +143,8 @@ defmodule Pleroma.ListTest do
|
|||
not_owner = insert(:user)
|
||||
member_1 = insert(:user)
|
||||
member_2 = insert(:user)
|
||||
{:ok, owned_list} = Pleroma.List.create("owned", owner)
|
||||
{:ok, not_owned_list} = Pleroma.List.create("not owned", not_owner)
|
||||
{:ok, owned_list} = Pleroma.List.create(%{title: "owned"}, owner)
|
||||
{:ok, not_owned_list} = Pleroma.List.create(%{title: "not owned"}, not_owner)
|
||||
{:ok, owned_list} = Pleroma.List.follow(owned_list, member_1)
|
||||
{:ok, owned_list} = Pleroma.List.follow(owned_list, member_2)
|
||||
{:ok, not_owned_list} = Pleroma.List.follow(not_owned_list, member_1)
|
||||
|
|
@ -123,14 +160,14 @@ defmodule Pleroma.ListTest do
|
|||
|
||||
test "get by ap_id" do
|
||||
user = insert(:user)
|
||||
{:ok, list} = Pleroma.List.create("foo", user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "foo"}, user)
|
||||
assert Pleroma.List.get_by_ap_id(list.ap_id) == list
|
||||
end
|
||||
|
||||
test "memberships" do
|
||||
user = insert(:user)
|
||||
member = insert(:user)
|
||||
{:ok, list} = Pleroma.List.create("foo", user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "foo"}, user)
|
||||
{:ok, list} = Pleroma.List.follow(list, member)
|
||||
|
||||
assert Pleroma.List.memberships(member) == [list.ap_id]
|
||||
|
|
@ -140,7 +177,7 @@ defmodule Pleroma.ListTest do
|
|||
user = insert(:user)
|
||||
member = insert(:user)
|
||||
|
||||
{:ok, list} = Pleroma.List.create("foo", user)
|
||||
{:ok, list} = Pleroma.List.create(%{title: "foo"}, user)
|
||||
{:ok, list} = Pleroma.List.follow(list, member)
|
||||
|
||||
assert Pleroma.List.member?(list, member)
|
||||
|
|
|
|||
|
|
@ -36,11 +36,12 @@ defmodule Pleroma.MarkerTest do
|
|||
insert(:notification, user: user, activity: insert(:note_activity))
|
||||
insert(:notification, user: user, activity: insert(:note_activity))
|
||||
insert(:marker, timeline: "home", user: user)
|
||||
%Marker{} = refreshed_marker = refresh_record(marker)
|
||||
|
||||
assert Marker.get_markers(
|
||||
user,
|
||||
["notifications"]
|
||||
) == [%Marker{refresh_record(marker) | unread_count: 2}]
|
||||
) == [%{refreshed_marker | unread_count: 2}]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ defmodule Pleroma.MigrationHelper.NotificationBackfillTest do
|
|||
{:ok, post} = CommonAPI.post(user, %{status: "yeah, @#{other_user.nickname}"})
|
||||
{:ok, chat} = CommonAPI.post_chat_message(user, other_user, "yo")
|
||||
{:ok, react} = CommonAPI.react_with_emoji(post.id, other_user, "☕")
|
||||
{:ok, like} = CommonAPI.favorite(other_user, post.id)
|
||||
{:ok, like} = CommonAPI.favorite(post.id, other_user)
|
||||
{:ok, react_2} = CommonAPI.react_with_emoji(post.id, other_user, "☕")
|
||||
|
||||
data =
|
||||
|
|
|
|||
|
|
@ -308,4 +308,37 @@ defmodule Pleroma.ModerationLogTest do
|
|||
assert log.data["message"] == "@#{moderator.nickname} deleted status ##{note.id}"
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_log_entry_message/1" do
|
||||
setup do
|
||||
moderator = insert(:user, is_moderator: true)
|
||||
[moderator: moderator]
|
||||
end
|
||||
|
||||
test "handles unknown action types gracefully", %{moderator: moderator} do
|
||||
log_entry = %ModerationLog{
|
||||
data: %{
|
||||
"actor" => %{"nickname" => moderator.nickname},
|
||||
"action" => "unknown_action",
|
||||
"some_data" => "test_value"
|
||||
}
|
||||
}
|
||||
|
||||
assert ModerationLog.get_log_entry_message(log_entry) =~ moderator.nickname
|
||||
assert ModerationLog.get_log_entry_message(log_entry) =~ "unknown_action"
|
||||
end
|
||||
|
||||
test "handles malformed log entries gracefully" do
|
||||
log_entry = %ModerationLog{
|
||||
data: %{
|
||||
"action" => "force_password_reset"
|
||||
# Missing "actor" and "subject" fields
|
||||
}
|
||||
}
|
||||
|
||||
message = ModerationLog.get_log_entry_message(log_entry)
|
||||
assert is_binary(message)
|
||||
assert message =~ "force_password_reset"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -17,9 +17,10 @@ defmodule Pleroma.NotificationTest do
|
|||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.MastodonAPI.NotificationView
|
||||
alias Pleroma.Web.Streamer
|
||||
|
||||
setup do
|
||||
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
|
||||
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
|
||||
:ok
|
||||
end
|
||||
|
||||
|
|
@ -165,7 +166,7 @@ defmodule Pleroma.NotificationTest do
|
|||
{:ok, _activity_two} = CommonAPI.repeat(activity_one.id, repeated_user)
|
||||
|
||||
{:ok, _edit_activity} =
|
||||
CommonAPI.update(user, activity_one, %{
|
||||
CommonAPI.update(activity_one, user, %{
|
||||
status: "hey @#{other_user.nickname}! mew mew"
|
||||
})
|
||||
|
||||
|
|
@ -180,8 +181,8 @@ defmodule Pleroma.NotificationTest do
|
|||
question = insert(:question, user: user1)
|
||||
activity = insert(:question_activity, question: question)
|
||||
|
||||
{:ok, _, _} = CommonAPI.vote(user2, question, [0])
|
||||
{:ok, _, _} = CommonAPI.vote(user3, question, [1])
|
||||
{:ok, _, _} = CommonAPI.vote(question, user2, [0])
|
||||
{:ok, _, _} = CommonAPI.vote(question, user3, [1])
|
||||
|
||||
{:ok, notifications} = Notification.create_poll_notifications(activity)
|
||||
|
||||
|
|
@ -209,7 +210,7 @@ defmodule Pleroma.NotificationTest do
|
|||
notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true}
|
||||
)
|
||||
|
||||
CommonAPI.follow(follower, followed)
|
||||
CommonAPI.follow(followed, follower)
|
||||
{:ok, activity} = CommonAPI.post(follower, %{status: "hey @#{followed.nickname}"})
|
||||
refute Notification.create_notification(activity, followed)
|
||||
end
|
||||
|
|
@ -222,7 +223,7 @@ defmodule Pleroma.NotificationTest do
|
|||
notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true}
|
||||
)
|
||||
|
||||
CommonAPI.follow(receiver, poster)
|
||||
CommonAPI.follow(poster, receiver)
|
||||
{:ok, activity} = CommonAPI.post(poster, %{status: "hey @#{receiver.nickname}"})
|
||||
assert Notification.create_notification(activity, receiver)
|
||||
end
|
||||
|
|
@ -238,7 +239,7 @@ defmodule Pleroma.NotificationTest do
|
|||
user = insert(:user)
|
||||
subscriber = insert(:user)
|
||||
|
||||
{:ok, _, _, _} = CommonAPI.follow(subscriber, user)
|
||||
{:ok, _, _, _} = CommonAPI.follow(user, subscriber)
|
||||
User.subscribe(subscriber, user)
|
||||
{:ok, status} = CommonAPI.post(user, %{status: "Akariiiin"})
|
||||
{:ok, [_notif]} = Notification.create_notifications(status)
|
||||
|
|
@ -295,7 +296,7 @@ defmodule Pleroma.NotificationTest do
|
|||
insert(:filter, user: user, phrase: "tesla", hide: true)
|
||||
|
||||
{:ok, activity_one} = CommonAPI.post(user, %{status: "wow tesla"})
|
||||
{:ok, activity_two} = CommonAPI.favorite(other_user, activity_one.id)
|
||||
{:ok, activity_two} = CommonAPI.favorite(activity_one.id, other_user)
|
||||
|
||||
{:ok, [notification]} = Notification.create_notifications(activity_two)
|
||||
|
||||
|
|
@ -309,7 +310,7 @@ defmodule Pleroma.NotificationTest do
|
|||
user = insert(:user)
|
||||
followed_user = insert(:user, is_locked: false)
|
||||
|
||||
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
|
||||
{:ok, _, _, _activity} = CommonAPI.follow(followed_user, user)
|
||||
assert FollowingRelationship.following?(user, followed_user)
|
||||
assert [notification] = Notification.for_user(followed_user)
|
||||
|
||||
|
|
@ -324,7 +325,7 @@ defmodule Pleroma.NotificationTest do
|
|||
user = insert(:user)
|
||||
followed_user = insert(:user, is_locked: true)
|
||||
|
||||
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
|
||||
{:ok, _, _, _activity} = CommonAPI.follow(followed_user, user)
|
||||
refute FollowingRelationship.following?(user, followed_user)
|
||||
assert [notification] = Notification.for_user(followed_user)
|
||||
|
||||
|
|
@ -339,19 +340,22 @@ defmodule Pleroma.NotificationTest do
|
|||
|> Repo.preload(:activity)
|
||||
|
||||
assert %{type: "follow"} =
|
||||
NotificationView.render("show.json", notification: notification, for: followed_user)
|
||||
NotificationView.render("show.json",
|
||||
notification: notification,
|
||||
for: followed_user
|
||||
)
|
||||
end
|
||||
|
||||
test "it doesn't create a notification for follow-unfollow-follow chains" do
|
||||
user = insert(:user)
|
||||
followed_user = insert(:user, is_locked: false)
|
||||
|
||||
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
|
||||
{:ok, _, _, _activity} = CommonAPI.follow(followed_user, user)
|
||||
assert FollowingRelationship.following?(user, followed_user)
|
||||
assert [notification] = Notification.for_user(followed_user)
|
||||
|
||||
CommonAPI.unfollow(user, followed_user)
|
||||
{:ok, _, _, _activity_dupe} = CommonAPI.follow(user, followed_user)
|
||||
CommonAPI.unfollow(followed_user, user)
|
||||
{:ok, _, _, _activity_dupe} = CommonAPI.follow(followed_user, user)
|
||||
|
||||
notification_id = notification.id
|
||||
assert [%{id: ^notification_id}] = Notification.for_user(followed_user)
|
||||
|
|
@ -360,7 +364,7 @@ defmodule Pleroma.NotificationTest do
|
|||
test "dismisses the notification on follow request rejection" do
|
||||
user = insert(:user, is_locked: true)
|
||||
follower = insert(:user)
|
||||
{:ok, _, _, _follow_activity} = CommonAPI.follow(follower, user)
|
||||
{:ok, _, _, _follow_activity} = CommonAPI.follow(user, follower)
|
||||
assert [_notification] = Notification.for_user(user)
|
||||
{:ok, _follower} = CommonAPI.reject_follow_request(follower, user)
|
||||
assert [] = Notification.for_user(user)
|
||||
|
|
@ -443,8 +447,7 @@ defmodule Pleroma.NotificationTest do
|
|||
|
||||
describe "set_read_up_to()" do
|
||||
test "it sets all notifications as read up to a specified notification ID" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
[user, other_user] = insert_pair(:user)
|
||||
|
||||
{:ok, _activity} =
|
||||
CommonAPI.post(user, %{
|
||||
|
|
@ -483,6 +486,37 @@ defmodule Pleroma.NotificationTest do
|
|||
|
||||
assert m.last_read_id == to_string(n2.id)
|
||||
end
|
||||
|
||||
@tag needs_streamer: true
|
||||
test "it sends updated marker to the 'user' and the 'user:notification' stream" do
|
||||
%{user: user, token: oauth_token} = oauth_access(["read"])
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, _activity} =
|
||||
CommonAPI.post(other_user, %{
|
||||
status: "hi @#{user.nickname}!"
|
||||
})
|
||||
|
||||
[%{id: notification_id}] = Notification.for_user(user)
|
||||
|
||||
notification_id = to_string(notification_id)
|
||||
|
||||
task =
|
||||
Task.async(fn ->
|
||||
{:ok, _topic} =
|
||||
Streamer.get_topic_and_add_socket("user:notification", user, oauth_token)
|
||||
|
||||
assert_receive {:text, event}, 4_000
|
||||
|
||||
assert %{"event" => "marker", "payload" => payload} = Jason.decode!(event)
|
||||
|
||||
assert %{"notifications" => %{"last_read_id" => ^notification_id}} =
|
||||
Jason.decode!(payload)
|
||||
end)
|
||||
|
||||
Notification.set_read_up_to(user, notification_id)
|
||||
Task.await(task)
|
||||
end
|
||||
end
|
||||
|
||||
describe "for_user_since/2" do
|
||||
|
|
@ -614,7 +648,7 @@ defmodule Pleroma.NotificationTest do
|
|||
status: "hey @#{other_user.nickname}!"
|
||||
})
|
||||
|
||||
{:ok, activity_two} = CommonAPI.favorite(third_user, activity_one.id)
|
||||
{:ok, activity_two} = CommonAPI.favorite(activity_one.id, third_user)
|
||||
|
||||
enabled_receivers = Notification.get_notified_from_activity(activity_two)
|
||||
|
||||
|
|
@ -690,7 +724,7 @@ defmodule Pleroma.NotificationTest do
|
|||
|
||||
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"})
|
||||
|
||||
{:ok, _} = CommonAPI.add_mute(other_user, activity)
|
||||
{:ok, _} = CommonAPI.add_mute(activity, other_user)
|
||||
|
||||
{:ok, same_context_activity} =
|
||||
CommonAPI.post(user, %{
|
||||
|
|
@ -745,7 +779,7 @@ defmodule Pleroma.NotificationTest do
|
|||
{:ok, _activity_two} = CommonAPI.repeat(activity_one.id, repeated_user)
|
||||
|
||||
{:ok, edit_activity} =
|
||||
CommonAPI.update(user, activity_one, %{
|
||||
CommonAPI.update(activity_one, user, %{
|
||||
status: "hey @#{other_user.nickname}! mew mew"
|
||||
})
|
||||
|
||||
|
|
@ -765,7 +799,7 @@ defmodule Pleroma.NotificationTest do
|
|||
|
||||
assert Enum.empty?(Notification.for_user(user))
|
||||
|
||||
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
|
||||
{:ok, _} = CommonAPI.favorite(activity.id, other_user)
|
||||
|
||||
assert length(Notification.for_user(user)) == 1
|
||||
|
||||
|
|
@ -782,7 +816,7 @@ defmodule Pleroma.NotificationTest do
|
|||
|
||||
assert Enum.empty?(Notification.for_user(user))
|
||||
|
||||
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
|
||||
{:ok, _} = CommonAPI.favorite(activity.id, other_user)
|
||||
|
||||
assert length(Notification.for_user(user)) == 1
|
||||
|
||||
|
|
@ -837,7 +871,7 @@ defmodule Pleroma.NotificationTest do
|
|||
|
||||
assert Enum.empty?(Notification.for_user(user))
|
||||
|
||||
{:error, :not_found} = CommonAPI.favorite(other_user, activity.id)
|
||||
{:error, :not_found} = CommonAPI.favorite(activity.id, other_user)
|
||||
|
||||
assert Enum.empty?(Notification.for_user(user))
|
||||
end
|
||||
|
|
@ -1087,7 +1121,7 @@ defmodule Pleroma.NotificationTest do
|
|||
another_user = insert(:user)
|
||||
|
||||
{:ok, activity} = CommonAPI.post(user, %{status: "Give me my cofe!"})
|
||||
{:ok, _} = CommonAPI.favorite(another_user, activity.id)
|
||||
{:ok, _} = CommonAPI.favorite(activity.id, another_user)
|
||||
|
||||
assert length(Notification.for_user(user)) == 1
|
||||
end
|
||||
|
|
@ -1098,7 +1132,7 @@ defmodule Pleroma.NotificationTest do
|
|||
|
||||
insert(:filter, user: followed_user, phrase: "test", hide: true)
|
||||
|
||||
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
|
||||
{:ok, _, _, _activity} = CommonAPI.follow(followed_user, user)
|
||||
refute FollowingRelationship.following?(user, followed_user)
|
||||
assert [notification] = Notification.for_user(followed_user)
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ defmodule Pleroma.Object.ContainmentTest do
|
|||
})
|
||||
end
|
||||
|
||||
test "rejects an empty actor list without crashing" do
|
||||
assert :error ==
|
||||
Containment.contain_origin("https://example.com/objects/1", %{"actor" => []})
|
||||
end
|
||||
|
||||
test "contain_origin_from_id() catches obvious spoofing attempts" do
|
||||
data = %{
|
||||
"id" => "http://example.com/~alyssa/activities/1234.json"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ defmodule Pleroma.Object.FetcherTest do
|
|||
use Pleroma.DataCase
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Instances
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Object.Fetcher
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidator
|
||||
|
|
@ -84,7 +83,6 @@ defmodule Pleroma.Object.FetcherTest do
|
|||
:ok
|
||||
end
|
||||
|
||||
@tag capture_log: true
|
||||
test "it works when fetching the OP actor errors out" do
|
||||
# Here we simulate a case where the author of the OP can't be read
|
||||
assert {:ok, _} =
|
||||
|
|
@ -101,7 +99,7 @@ defmodule Pleroma.Object.FetcherTest do
|
|||
test "it returns thread depth exceeded error if thread depth is exceeded" do
|
||||
clear_config([:instance, :federation_incoming_replies_max_depth], 0)
|
||||
|
||||
assert {:error, :allowed_depth} = Fetcher.fetch_object_from_id(@ap_id, depth: 1)
|
||||
assert {:allowed_depth, false} = Fetcher.fetch_object_from_id(@ap_id, depth: 1)
|
||||
end
|
||||
|
||||
test "it fetches object if max thread depth is restricted to 0 and depth is not specified" do
|
||||
|
|
@ -119,15 +117,18 @@ defmodule Pleroma.Object.FetcherTest do
|
|||
|
||||
describe "actor origin containment" do
|
||||
test "it rejects objects with a bogus origin" do
|
||||
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json")
|
||||
{:containment, :error} =
|
||||
Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json")
|
||||
end
|
||||
|
||||
test "it rejects objects when attributedTo is wrong (variant 1)" do
|
||||
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json")
|
||||
{:containment, :error} =
|
||||
Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json")
|
||||
end
|
||||
|
||||
test "it rejects objects when attributedTo is wrong (variant 2)" do
|
||||
{:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity3.json")
|
||||
{:containment, :error} =
|
||||
Fetcher.fetch_object_from_id("https://info.pleroma.site/activity3.json")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -151,28 +152,102 @@ defmodule Pleroma.Object.FetcherTest do
|
|||
clear_config([:mrf_keyword, :reject], ["yeah"])
|
||||
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
|
||||
|
||||
assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} ==
|
||||
assert {:transmogrifier, {:reject, "[KeywordPolicy] Matches with rejected keyword"}} ==
|
||||
Fetcher.fetch_object_from_id(
|
||||
"http://mastodon.example.org/@admin/99541947525187367"
|
||||
)
|
||||
end
|
||||
|
||||
test "it does not fetch a spoofed object uploaded on an instance as an attachment" do
|
||||
assert {:error, _} =
|
||||
assert {:fetch, {:error, {:content_type, "application/json"}}} =
|
||||
Fetcher.fetch_object_from_id(
|
||||
"https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json"
|
||||
)
|
||||
end
|
||||
|
||||
test "it resets instance reachability on successful fetch" do
|
||||
id = "http://mastodon.example.org/@admin/99541947525187367"
|
||||
Instances.set_consistently_unreachable(id)
|
||||
refute Instances.reachable?(id)
|
||||
test "it does not fetch from local instance" do
|
||||
local_url = Pleroma.Web.Endpoint.url() <> "/objects/local_resource"
|
||||
|
||||
{:ok, _object} =
|
||||
Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
|
||||
assert {:fetch, {:error, "Trying to fetch local resource"}} =
|
||||
Fetcher.fetch_object_from_id(local_url)
|
||||
end
|
||||
|
||||
assert Instances.reachable?(id)
|
||||
test "it validates content-type headers according to ActivityPub spec" do
|
||||
# Setup a mock for an object with invalid content-type
|
||||
mock(fn
|
||||
%{method: :get, url: "https://example.com/objects/invalid-content-type"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
# Not a valid AP content-type
|
||||
headers: [{"content-type", "application/json"}],
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
"id" => "https://example.com/objects/invalid-content-type",
|
||||
"type" => "Note",
|
||||
"content" => "This has an invalid content type",
|
||||
"actor" => "https://example.com/users/actor",
|
||||
"attributedTo" => "https://example.com/users/actor"
|
||||
})
|
||||
}
|
||||
end)
|
||||
|
||||
assert {:fetch, {:error, {:content_type, "application/json"}}} =
|
||||
Fetcher.fetch_object_from_id("https://example.com/objects/invalid-content-type")
|
||||
end
|
||||
|
||||
test "it accepts objects with application/ld+json and ActivityStreams profile" do
|
||||
# Setup a mock for an object with ld+json content-type and AS profile
|
||||
mock(fn
|
||||
%{method: :get, url: "https://example.com/objects/valid-ld-json"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
headers: [
|
||||
{"content-type",
|
||||
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""}
|
||||
],
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
"id" => "https://example.com/objects/valid-ld-json",
|
||||
"type" => "Note",
|
||||
"content" => "This has a valid ld+json content type",
|
||||
"actor" => "https://example.com/users/actor",
|
||||
"attributedTo" => "https://example.com/users/actor"
|
||||
})
|
||||
}
|
||||
end)
|
||||
|
||||
# This should pass if content-type validation works correctly
|
||||
assert {:ok, object} =
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(
|
||||
"https://example.com/objects/valid-ld-json"
|
||||
)
|
||||
|
||||
assert object["content"] == "This has a valid ld+json content type"
|
||||
end
|
||||
|
||||
test "it rejects objects with no content-type header" do
|
||||
# Setup a mock for an object with no content-type header
|
||||
mock(fn
|
||||
%{method: :get, url: "https://example.com/objects/no-content-type"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
# No content-type header
|
||||
headers: [],
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
"id" => "https://example.com/objects/no-content-type",
|
||||
"type" => "Note",
|
||||
"content" => "This has no content type header",
|
||||
"actor" => "https://example.com/users/actor",
|
||||
"attributedTo" => "https://example.com/users/actor"
|
||||
})
|
||||
}
|
||||
end)
|
||||
|
||||
# We want to test that the request fails with a missing content-type error
|
||||
# but the actual error is {:fetch, {:error, nil}} - we'll check for this format
|
||||
result = Fetcher.fetch_object_from_id("https://example.com/objects/no-content-type")
|
||||
assert {:fetch, {:error, nil}} = result
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -532,6 +607,110 @@ defmodule Pleroma.Object.FetcherTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "cross-domain redirect handling" do
|
||||
setup do
|
||||
mock(fn
|
||||
# Cross-domain redirect with original domain in id
|
||||
%{method: :get, url: "https://original.test/objects/123"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
url: "https://media.test/objects/123",
|
||||
headers: [{"content-type", "application/activity+json"}],
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
"id" => "https://original.test/objects/123",
|
||||
"type" => "Note",
|
||||
"content" => "This is redirected content",
|
||||
"actor" => "https://original.test/users/actor",
|
||||
"attributedTo" => "https://original.test/users/actor"
|
||||
})
|
||||
}
|
||||
|
||||
# Cross-domain redirect with final domain in id
|
||||
%{method: :get, url: "https://original.test/objects/final-domain-id"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
url: "https://media.test/objects/final-domain-id",
|
||||
headers: [{"content-type", "application/activity+json"}],
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
"id" => "https://media.test/objects/final-domain-id",
|
||||
"type" => "Note",
|
||||
"content" => "This has final domain in id",
|
||||
"actor" => "https://original.test/users/actor",
|
||||
"attributedTo" => "https://original.test/users/actor"
|
||||
})
|
||||
}
|
||||
|
||||
# No redirect - same domain
|
||||
%{method: :get, url: "https://original.test/objects/same-domain-redirect"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
url: "https://original.test/objects/different-path",
|
||||
headers: [{"content-type", "application/activity+json"}],
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
"id" => "https://original.test/objects/same-domain-redirect",
|
||||
"type" => "Note",
|
||||
"content" => "This has a same-domain redirect",
|
||||
"actor" => "https://original.test/users/actor",
|
||||
"attributedTo" => "https://original.test/users/actor"
|
||||
})
|
||||
}
|
||||
|
||||
# Test case with missing url field in response (common in tests)
|
||||
%{method: :get, url: "https://original.test/objects/missing-url"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
# No url field
|
||||
headers: [{"content-type", "application/activity+json"}],
|
||||
body:
|
||||
Jason.encode!(%{
|
||||
"id" => "https://original.test/objects/missing-url",
|
||||
"type" => "Note",
|
||||
"content" => "This has no URL field in response",
|
||||
"actor" => "https://original.test/users/actor",
|
||||
"attributedTo" => "https://original.test/users/actor"
|
||||
})
|
||||
}
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "it rejects objects from cross-domain redirects with original domain in id" do
|
||||
assert {:error, {:cross_domain_redirect, true}} =
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(
|
||||
"https://original.test/objects/123"
|
||||
)
|
||||
end
|
||||
|
||||
test "it rejects objects from cross-domain redirects with final domain in id" do
|
||||
assert {:error, {:cross_domain_redirect, true}} =
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(
|
||||
"https://original.test/objects/final-domain-id"
|
||||
)
|
||||
end
|
||||
|
||||
test "it accepts objects with same-domain redirects" do
|
||||
assert {:ok, data} =
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(
|
||||
"https://original.test/objects/same-domain-redirect"
|
||||
)
|
||||
|
||||
assert data["content"] == "This has a same-domain redirect"
|
||||
end
|
||||
|
||||
test "it handles responses without URL field (common in tests)" do
|
||||
assert {:ok, data} =
|
||||
Fetcher.fetch_and_contain_remote_object_from_id(
|
||||
"https://original.test/objects/missing-url"
|
||||
)
|
||||
|
||||
assert data["content"] == "This has no URL field in response"
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch with history" do
|
||||
setup do
|
||||
object2 = %{
|
||||
|
|
|
|||
|
|
@ -6,12 +6,10 @@ defmodule Pleroma.ObjectTest do
|
|||
use Pleroma.DataCase
|
||||
use Oban.Testing, repo: Pleroma.Repo
|
||||
|
||||
import ExUnit.CaptureLog
|
||||
import Mox
|
||||
import Pleroma.Factory
|
||||
import Tesla.Mock
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Hashtag
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
|
|
@ -158,7 +156,7 @@ defmodule Pleroma.ObjectTest do
|
|||
|
||||
uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
|
||||
|
||||
File.mkdir_p!(uploads_dir)
|
||||
Pleroma.Backports.mkdir_p!(uploads_dir)
|
||||
|
||||
file = %Plug.Upload{
|
||||
content_type: "image/jpeg",
|
||||
|
|
@ -176,8 +174,9 @@ defmodule Pleroma.ObjectTest do
|
|||
|
||||
filename = Path.basename(href)
|
||||
|
||||
assert {:ok, files} = File.ls(uploads_dir)
|
||||
assert filename in files
|
||||
expected_path = Path.join([uploads_dir, Pleroma.Upload.Filter.Dedupe.shard_path(filename)])
|
||||
|
||||
assert File.exists?(expected_path)
|
||||
|
||||
Object.delete(note)
|
||||
|
||||
|
|
@ -185,8 +184,7 @@ defmodule Pleroma.ObjectTest do
|
|||
|
||||
assert Object.get_by_id(note.id).data["deleted"]
|
||||
assert Object.get_by_id(attachment.id) == nil
|
||||
assert {:ok, files} = File.ls(uploads_dir)
|
||||
refute filename in files
|
||||
refute File.exists?(expected_path)
|
||||
end
|
||||
|
||||
test "with objects that have legacy data.url attribute" do
|
||||
|
|
@ -282,148 +280,6 @@ defmodule Pleroma.ObjectTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "get_by_id_and_maybe_refetch" do
|
||||
setup do
|
||||
mock(fn
|
||||
%{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
body: File.read!("test/fixtures/tesla_mock/poll_original.json"),
|
||||
headers: HttpRequestMock.activitypub_object_headers()
|
||||
}
|
||||
|
||||
env ->
|
||||
apply(HttpRequestMock, :request, [env])
|
||||
end)
|
||||
|
||||
mock_modified = fn resp ->
|
||||
mock(fn
|
||||
%{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
|
||||
resp
|
||||
|
||||
env ->
|
||||
apply(HttpRequestMock, :request, [env])
|
||||
end)
|
||||
end
|
||||
|
||||
on_exit(fn -> mock(fn env -> apply(HttpRequestMock, :request, [env]) end) end)
|
||||
|
||||
[mock_modified: mock_modified]
|
||||
end
|
||||
|
||||
test "refetches if the time since the last refetch is greater than the interval", %{
|
||||
mock_modified: mock_modified
|
||||
} do
|
||||
%Object{} =
|
||||
object =
|
||||
Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
|
||||
fetch: true
|
||||
)
|
||||
|
||||
Object.set_cache(object)
|
||||
|
||||
assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
|
||||
assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
|
||||
|
||||
mock_modified.(%Tesla.Env{
|
||||
status: 200,
|
||||
body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
|
||||
headers: HttpRequestMock.activitypub_object_headers()
|
||||
})
|
||||
|
||||
updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
|
||||
object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
|
||||
assert updated_object == object_in_cache
|
||||
assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
|
||||
assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
|
||||
end
|
||||
|
||||
test "returns the old object if refetch fails", %{mock_modified: mock_modified} do
|
||||
%Object{} =
|
||||
object =
|
||||
Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
|
||||
fetch: true
|
||||
)
|
||||
|
||||
Object.set_cache(object)
|
||||
|
||||
assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
|
||||
assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
|
||||
|
||||
assert capture_log(fn ->
|
||||
mock_modified.(%Tesla.Env{status: 404, body: ""})
|
||||
|
||||
updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
|
||||
object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
|
||||
assert updated_object == object_in_cache
|
||||
assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
|
||||
assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
|
||||
end) =~
|
||||
"[error] Couldn't refresh https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"
|
||||
end
|
||||
|
||||
test "does not refetch if the time since the last refetch is greater than the interval", %{
|
||||
mock_modified: mock_modified
|
||||
} do
|
||||
%Object{} =
|
||||
object =
|
||||
Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
|
||||
fetch: true
|
||||
)
|
||||
|
||||
Object.set_cache(object)
|
||||
|
||||
assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
|
||||
assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
|
||||
|
||||
mock_modified.(%Tesla.Env{
|
||||
status: 200,
|
||||
body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
|
||||
headers: HttpRequestMock.activitypub_object_headers()
|
||||
})
|
||||
|
||||
updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: 100)
|
||||
object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
|
||||
assert updated_object == object_in_cache
|
||||
assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
|
||||
assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
|
||||
end
|
||||
|
||||
test "preserves internal fields on refetch", %{mock_modified: mock_modified} do
|
||||
%Object{} =
|
||||
object =
|
||||
Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d",
|
||||
fetch: true
|
||||
)
|
||||
|
||||
Object.set_cache(object)
|
||||
|
||||
assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
|
||||
assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
|
||||
|
||||
user = insert(:user)
|
||||
activity = Activity.get_create_by_object_ap_id(object.data["id"])
|
||||
{:ok, activity} = CommonAPI.favorite(user, activity.id)
|
||||
object = Object.get_by_ap_id(activity.data["object"])
|
||||
|
||||
assert object.data["like_count"] == 1
|
||||
|
||||
mock_modified.(%Tesla.Env{
|
||||
status: 200,
|
||||
body: File.read!("test/fixtures/tesla_mock/poll_modified.json"),
|
||||
headers: HttpRequestMock.activitypub_object_headers()
|
||||
})
|
||||
|
||||
updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
|
||||
object_in_cache = Object.get_cached_by_ap_id(object.data["id"])
|
||||
assert updated_object == object_in_cache
|
||||
assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
|
||||
assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
|
||||
|
||||
assert updated_object.data["like_count"] == 1
|
||||
end
|
||||
end
|
||||
|
||||
describe ":hashtags association" do
|
||||
test "Hashtag records are created with Object record and updated on its change" do
|
||||
user = insert(:user)
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.OTPVersionTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Pleroma.OTPVersion
|
||||
|
||||
describe "check/1" do
|
||||
test "22.4" do
|
||||
assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.4"]) ==
|
||||
"22.4"
|
||||
end
|
||||
|
||||
test "22.1" do
|
||||
assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/22.1"]) ==
|
||||
"22.1"
|
||||
end
|
||||
|
||||
test "21.1" do
|
||||
assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/21.1"]) ==
|
||||
"21.1"
|
||||
end
|
||||
|
||||
test "23.0" do
|
||||
assert OTPVersion.get_version_from_files(["test/fixtures/warnings/otp_version/23.0"]) ==
|
||||
"23.0"
|
||||
end
|
||||
|
||||
test "with nonexistent file" do
|
||||
assert OTPVersion.get_version_from_files([
|
||||
"test/fixtures/warnings/otp_version/non-exising",
|
||||
"test/fixtures/warnings/otp_version/22.4"
|
||||
]) == "22.4"
|
||||
end
|
||||
|
||||
test "empty paths" do
|
||||
assert OTPVersion.get_version_from_files([]) == nil
|
||||
end
|
||||
end
|
||||
end
|
||||
293
test/pleroma/pleroma_ctl_test.exs
Normal file
293
test/pleroma/pleroma_ctl_test.exs
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.PleromaCtlTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
@pleroma_ctl Path.expand("../../rel/files/bin/pleroma_ctl", __DIR__)
|
||||
|
||||
setup do
|
||||
tmp_dir =
|
||||
Path.join(System.tmp_dir!(), "pleroma_ctl_test_#{System.unique_integer([:positive])}")
|
||||
|
||||
release_root = Path.join(tmp_dir, "release")
|
||||
bin_dir = Path.join(release_root, "bin")
|
||||
|
||||
File.mkdir_p!(bin_dir)
|
||||
File.cp!(@pleroma_ctl, Path.join(bin_dir, "pleroma_ctl"))
|
||||
File.chmod!(Path.join(bin_dir, "pleroma_ctl"), 0o755)
|
||||
|
||||
on_exit(fn -> File.rm_rf!(tmp_dir) end)
|
||||
|
||||
{:ok, tmp_dir: tmp_dir, release_root: release_root, bin_dir: bin_dir}
|
||||
end
|
||||
|
||||
test "update downloads branch-scoped latest OTP package", %{
|
||||
tmp_dir: tmp_dir,
|
||||
bin_dir: bin_dir,
|
||||
release_root: release_root
|
||||
} do
|
||||
stubs_dir = Path.join(tmp_dir, "stubs")
|
||||
curl_args_path = Path.join(tmp_dir, "curl.args")
|
||||
File.mkdir_p!(stubs_dir)
|
||||
write_update_stubs(stubs_dir, curl_args_path, "unused", "glibc")
|
||||
|
||||
update_tmp_dir = Path.join(tmp_dir, "update_tmp")
|
||||
File.mkdir_p!(update_tmp_dir)
|
||||
|
||||
{output, status} =
|
||||
System.cmd(
|
||||
Path.join(bin_dir, "pleroma_ctl"),
|
||||
[
|
||||
"update",
|
||||
"--branch",
|
||||
"develop",
|
||||
"--flavour",
|
||||
"amd64",
|
||||
"--tmp-dir",
|
||||
update_tmp_dir,
|
||||
"--no-rm"
|
||||
],
|
||||
env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}],
|
||||
stderr_to_stdout: true
|
||||
)
|
||||
|
||||
assert status == 0, output
|
||||
assert File.exists?(Path.join(release_root, "bin/marker"))
|
||||
|
||||
assert ["-fL", url, "-o", artifact_path] =
|
||||
curl_args_path
|
||||
|> File.read!()
|
||||
|> String.split("\n", trim: true)
|
||||
|
||||
assert url ==
|
||||
"https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-develop-amd64/latest/pleroma.zip"
|
||||
|
||||
assert artifact_path == Path.join(update_tmp_dir, "pleroma.zip")
|
||||
end
|
||||
|
||||
test "update detects stable branch and local flavour", %{
|
||||
tmp_dir: tmp_dir,
|
||||
bin_dir: bin_dir,
|
||||
release_root: release_root
|
||||
} do
|
||||
stubs_dir = Path.join(tmp_dir, "stubs")
|
||||
curl_args_path = Path.join(tmp_dir, "curl.args")
|
||||
File.mkdir_p!(stubs_dir)
|
||||
write_update_stubs(stubs_dir, curl_args_path, "x86_64", "glibc")
|
||||
write_start_erl_data(release_root, "2.10.0")
|
||||
|
||||
{output, status} =
|
||||
System.cmd(
|
||||
Path.join(bin_dir, "pleroma_ctl"),
|
||||
["update", "--tmp-dir", create_update_tmp_dir(tmp_dir), "--no-rm"],
|
||||
env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}],
|
||||
stderr_to_stdout: true
|
||||
)
|
||||
|
||||
assert status == 0, output
|
||||
|
||||
assert curl_url(curl_args_path) ==
|
||||
"https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-stable-amd64/latest/pleroma.zip"
|
||||
end
|
||||
|
||||
test "update detects develop branch and musl arm flavour", %{
|
||||
tmp_dir: tmp_dir,
|
||||
bin_dir: bin_dir,
|
||||
release_root: release_root
|
||||
} do
|
||||
stubs_dir = Path.join(tmp_dir, "stubs")
|
||||
curl_args_path = Path.join(tmp_dir, "curl.args")
|
||||
File.mkdir_p!(stubs_dir)
|
||||
write_update_stubs(stubs_dir, curl_args_path, "armv7l", "musl")
|
||||
write_start_erl_data(release_root, "2.10.0.develop")
|
||||
|
||||
{output, status} =
|
||||
System.cmd(
|
||||
Path.join(bin_dir, "pleroma_ctl"),
|
||||
["update", "--tmp-dir", create_update_tmp_dir(tmp_dir), "--no-rm"],
|
||||
env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}],
|
||||
stderr_to_stdout: true
|
||||
)
|
||||
|
||||
assert status == 0, output
|
||||
|
||||
assert curl_url(curl_args_path) ==
|
||||
"https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-develop-arm-musl/latest/pleroma.zip"
|
||||
end
|
||||
|
||||
test "update with zip URL bypasses branch and flavour detection", %{
|
||||
tmp_dir: tmp_dir,
|
||||
bin_dir: bin_dir,
|
||||
release_root: release_root
|
||||
} do
|
||||
stubs_dir = Path.join(tmp_dir, "stubs")
|
||||
curl_args_path = Path.join(tmp_dir, "curl.args")
|
||||
File.mkdir_p!(stubs_dir)
|
||||
write_update_stubs(stubs_dir, curl_args_path, "unsupported-arch", "unsupported-libc")
|
||||
write_start_erl_data(release_root, "2.10.0.custombranch")
|
||||
|
||||
custom_url = "https://example.test/custom.zip"
|
||||
|
||||
{output, status} =
|
||||
System.cmd(
|
||||
Path.join(bin_dir, "pleroma_ctl"),
|
||||
[
|
||||
"update",
|
||||
"--zip-url",
|
||||
custom_url,
|
||||
"--tmp-dir",
|
||||
create_update_tmp_dir(tmp_dir),
|
||||
"--no-rm"
|
||||
],
|
||||
env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}],
|
||||
stderr_to_stdout: true
|
||||
)
|
||||
|
||||
assert status == 0, output
|
||||
assert curl_url(curl_args_path) == custom_url
|
||||
end
|
||||
|
||||
test "passes arguments with spaces and Elixir string metacharacters", %{
|
||||
tmp_dir: tmp_dir,
|
||||
bin_dir: bin_dir
|
||||
} do
|
||||
capture_path = Path.join(tmp_dir, "captured_args")
|
||||
eval_path = Path.join(tmp_dir, "pleroma_ctl_eval.exs")
|
||||
|
||||
write_executable(Path.join(bin_dir, "pleroma"), """
|
||||
#!/bin/sh
|
||||
{
|
||||
printf '%s\n' 'defmodule Pleroma.ReleaseTasks do'
|
||||
printf '%s\n' ' def run(args), do: File.write!(System.fetch_env!("PLEROMA_CTL_CAPTURE"), :erlang.term_to_binary(args))'
|
||||
printf '%s\n' 'end'
|
||||
printf '%s\n' "$2"
|
||||
} > "$PLEROMA_CTL_EVAL_FILE"
|
||||
|
||||
exec elixir "$PLEROMA_CTL_EVAL_FILE"
|
||||
""")
|
||||
|
||||
{output, status} =
|
||||
System.cmd(
|
||||
Path.join(bin_dir, "pleroma_ctl"),
|
||||
[
|
||||
"user",
|
||||
"",
|
||||
"has space",
|
||||
~s(has "quote"),
|
||||
~s(has \\ backslash),
|
||||
~S(#{:not_interpolated})
|
||||
],
|
||||
env: [
|
||||
{"PLEROMA_CTL_CAPTURE", capture_path},
|
||||
{"PLEROMA_CTL_EVAL_FILE", eval_path}
|
||||
],
|
||||
stderr_to_stdout: true
|
||||
)
|
||||
|
||||
assert status == 0, output
|
||||
|
||||
assert capture_path
|
||||
|> File.read!()
|
||||
|> :erlang.binary_to_term() == [
|
||||
"user",
|
||||
"",
|
||||
"has space",
|
||||
~s(has "quote"),
|
||||
~s(has \\ backslash),
|
||||
~S(#{:not_interpolated})
|
||||
]
|
||||
end
|
||||
|
||||
defp write_executable(path, contents) do
|
||||
File.write!(path, contents)
|
||||
File.chmod!(path, 0o755)
|
||||
end
|
||||
|
||||
defp write_start_erl_data(release_root, version) do
|
||||
releases_dir = Path.join(release_root, "releases")
|
||||
File.mkdir_p!(releases_dir)
|
||||
File.write!(Path.join(releases_dir, "start_erl.data"), "erts-15.0 #{version}\n")
|
||||
end
|
||||
|
||||
defp create_update_tmp_dir(tmp_dir) do
|
||||
update_tmp_dir = Path.join(tmp_dir, "update_tmp")
|
||||
File.mkdir_p!(update_tmp_dir)
|
||||
update_tmp_dir
|
||||
end
|
||||
|
||||
defp write_update_stubs(stubs_dir, curl_args_path, arch, libc) do
|
||||
write_executable(Path.join(stubs_dir, "curl"), """
|
||||
#!/bin/sh
|
||||
printf '%s\n' "$@" > "#{curl_args_path}"
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-o)
|
||||
artifact="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
: > "$artifact"
|
||||
""")
|
||||
|
||||
write_executable(Path.join(stubs_dir, "unzip"), """
|
||||
#!/bin/sh
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-d)
|
||||
dest="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
mkdir -p "$dest/release/bin"
|
||||
printf 'marker' > "$dest/release/bin/marker"
|
||||
""")
|
||||
|
||||
write_executable(Path.join(stubs_dir, "uname"), """
|
||||
#!/bin/sh
|
||||
printf '%s\n' '#{arch}'
|
||||
""")
|
||||
|
||||
write_executable(Path.join(stubs_dir, "getconf"), getconf_stub(libc))
|
||||
|
||||
write_executable(Path.join(stubs_dir, "ldd"), """
|
||||
#!/bin/sh
|
||||
printf '%s\n' 'musl libc (mock)'
|
||||
""")
|
||||
end
|
||||
|
||||
defp getconf_stub("glibc") do
|
||||
"""
|
||||
#!/bin/sh
|
||||
printf '%s\n' 'glibc 2.40'
|
||||
"""
|
||||
end
|
||||
|
||||
defp getconf_stub(_libc) do
|
||||
"""
|
||||
#!/bin/sh
|
||||
exit 1
|
||||
"""
|
||||
end
|
||||
|
||||
defp curl_url(curl_args_path) do
|
||||
["-fL", url, "-o", _artifact_path] =
|
||||
curl_args_path
|
||||
|> File.read!()
|
||||
|> String.split("\n", trim: true)
|
||||
|
||||
url
|
||||
end
|
||||
end
|
||||
19
test/pleroma/release_task_test.exs
Normal file
19
test/pleroma/release_task_test.exs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.ReleaseTaskTest do
|
||||
use Pleroma.DataCase, async: true
|
||||
|
||||
alias Pleroma.ReleaseTasks
|
||||
|
||||
test "finding the module" do
|
||||
task = "search.meilisearch"
|
||||
assert Mix.Tasks.Pleroma.Search.Meilisearch == ReleaseTasks.find_module(task)
|
||||
|
||||
task = "user"
|
||||
assert Mix.Tasks.Pleroma.User == ReleaseTasks.find_module(task)
|
||||
|
||||
refute ReleaseTasks.find_module("doesnt.exist")
|
||||
end
|
||||
end
|
||||
|
|
@ -3,12 +3,11 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Repo.Migrations.AutolinkerToLinkifyTest do
|
||||
use Pleroma.DataCase
|
||||
use Pleroma.DataCase, async: false
|
||||
import Pleroma.Factory
|
||||
import Pleroma.Tests.Helpers
|
||||
alias Pleroma.ConfigDB
|
||||
|
||||
setup do: clear_config(Pleroma.Formatter)
|
||||
setup_all do: require_migration("20200716195806_autolinker_to_linkify")
|
||||
|
||||
test "change/0 converts auto_linker opts for Pleroma.Formatter", %{migration: migration} do
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue