Merge remote-tracking branch 'origin/develop' into pr-7444-finish

This commit is contained in:
Lain Soykaf 2026-07-26 13:20:50 +04:00
commit 7a741736ea
No known key found for this signature in database
1544 changed files with 43219 additions and 9351 deletions

BIN
test/fixtures/DSCN0010.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

View 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&#39;m a big believer in &quot;Do as I say, not as I <strong>did</strong>&quot;.</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&#39;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

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

112
test/fixtures/denniskoch.json vendored Normal file
View 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"
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View 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"
}

View 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"
}

View 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"
}

View 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
}
}

View 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"
}

File diff suppressed because one or more lines are too long

18
test/fixtures/server.pem vendored Normal file
View 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-----

View 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}]

View 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
View 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;

View file

@ -13,7 +13,7 @@
"directMessage": "litepub:directMessage"
}
],
"id": "http://localhost:8080/followers/fuser3",
"id": "https://remote.org/followers/fuser3",
"type": "OrderedCollection",
"totalItems": 296
}

View file

@ -13,7 +13,7 @@
"directMessage": "litepub:directMessage"
}
],
"id": "http://localhost:8080/following/fuser3",
"id": "https://remote.org/following/fuser3",
"type": "OrderedCollection",
"totalItems": 32
}

View 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"
}

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1 +0,0 @@
21.1

View file

@ -1 +0,0 @@
22.1

View file

@ -1 +0,0 @@
22.4

View file

@ -1 +0,0 @@
23.0

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View 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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View 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

View 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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -27,6 +27,11 @@ defmodule Pleroma.Object.ContainmentTest do
})
end
test "rejects an empty actor list without crashing" do
assert :error ==
Containment.contain_origin("https://example.com/objects/1", %{"actor" => []})
end
test "contain_origin_from_id() catches obvious spoofing attempts" do
data = %{
"id" => "http://example.com/~alyssa/activities/1234.json"

View file

@ -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 = %{

View file

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

View file

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

View 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

View 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

View file

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