diff options
author | Lain Soykaf <lain@lain.com> | 2024-05-27 16:23:40 +0400 |
---|---|---|
committer | Lain Soykaf <lain@lain.com> | 2024-05-27 16:23:40 +0400 |
commit | 825b4122a5cda720cb567196ad34e456a68f5dc6 (patch) | |
tree | 9b41826b70cf7825db5535cd0383b1aebc48806d /test | |
parent | 21d9091f5e422493ff69fe59db9c965e0d511369 (diff) | |
parent | 6757382abec9ca47a9025b8bed61047414cee20f (diff) | |
download | pleroma-825b4122a5cda720cb567196ad34e456a68f5dc6.tar.gz pleroma-825b4122a5cda720cb567196ad34e456a68f5dc6.zip |
Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into pleroma-ipfs_uploader
Diffstat (limited to 'test')
264 files changed, 14156 insertions, 2117 deletions
diff --git a/test/fixtures/ccworld-ap-bridge_note.json b/test/fixtures/ccworld-ap-bridge_note.json new file mode 100644 index 000000000..4b13b4bc1 --- /dev/null +++ b/test/fixtures/ccworld-ap-bridge_note.json @@ -0,0 +1 @@ +{"@context":"https://www.w3.org/ns/activitystreams","type":"Note","id":"https://cc.mkdir.uk/ap/note/e5d1d0a1-1ab3-4498-9949-588e3fdea286","attributedTo":"https://cc.mkdir.uk/ap/acct/hiira","inReplyTo":"","quoteUrl":"","content":"おはコンー","published":"2024-01-19T22:08:05Z","to":["https://www.w3.org/ns/activitystreams#Public"],"tag":null,"attachment":[],"object":null} diff --git a/test/fixtures/config/temp.secret.exs b/test/fixtures/config/temp.secret.exs index d4140d0c4..e5709ba6f 100644 --- a/test/fixtures/config/temp.secret.exs +++ b/test/fixtures/config/temp.secret.exs @@ -8,8 +8,6 @@ config :pleroma, :first_setting, key: "value", key2: [Pleroma.Repo] config :pleroma, :second_setting, key: "value2", key2: ["Activity"] -config :quack, level: :info - config :pleroma, Pleroma.Repo, pool: Ecto.Adapters.SQL.Sandbox config :postgrex, :json_library, Poison diff --git a/test/fixtures/create-pleroma-reply-to-misskey-thread.json b/test/fixtures/create-pleroma-reply-to-misskey-thread.json new file mode 100644 index 000000000..0c31efa76 --- /dev/null +++ b/test/fixtures/create-pleroma-reply-to-misskey-thread.json @@ -0,0 +1,61 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://p.helene.moe/schemas/litepub-0.1.jsonld", + { + "@language": "und" + } + ], + "actor": "https://p.helene.moe/users/helene", + "attachment": [], + "attributedTo": "https://p.helene.moe/users/helene", + "cc": [ + "https://p.helene.moe/users/helene/followers" + ], + "context": "https://p.helene.moe/contexts/cc324643-5583-4c3f-91d2-c6ed37db159d", + "conversation": "https://p.helene.moe/contexts/cc324643-5583-4c3f-91d2-c6ed37db159d", + "directMessage": false, + "id": "https://p.helene.moe/activities/5f80db86-a9bb-4883-9845-fbdbd1478f3a", + "object": { + "actor": "https://p.helene.moe/users/helene", + "attachment": [], + "attributedTo": "https://p.helene.moe/users/helene", + "cc": [ + "https://p.helene.moe/users/helene/followers" + ], + "content": "<span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AHntpQ4T3J4OSnpgMC\" href=\"https://mk.absturztau.be/@mametsuko\" rel=\"ugc\">@<span>mametsuko</span></a></span> meow", + "context": "https://p.helene.moe/contexts/cc324643-5583-4c3f-91d2-c6ed37db159d", + "conversation": "https://p.helene.moe/contexts/cc324643-5583-4c3f-91d2-c6ed37db159d", + "id": "https://p.helene.moe/objects/fd5910ac-d9dc-412e-8d1d-914b203296c4", + "inReplyTo": "https://mk.absturztau.be/notes/93e7nm8wqg", + "published": "2022-08-02T13:46:58.403996Z", + "sensitive": null, + "source": "@mametsuko@mk.absturztau.be meow", + "summary": "", + "tag": [ + { + "href": "https://mk.absturztau.be/users/8ozbzjs3o8", + "name": "@mametsuko@mk.absturztau.be", + "type": "Mention" + } + ], + "to": [ + "https://mk.absturztau.be/users/8ozbzjs3o8", + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Note" + }, + "published": "2022-08-02T13:46:58.403883Z", + "tag": [ + { + "href": "https://mk.absturztau.be/users/8ozbzjs3o8", + "name": "@mametsuko@mk.absturztau.be", + "type": "Mention" + } + ], + "to": [ + "https://mk.absturztau.be/users/8ozbzjs3o8", + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Create" +}
\ No newline at end of file diff --git a/test/fixtures/custom-emoji-reaction.json b/test/fixtures/custom-emoji-reaction.json new file mode 100644 index 000000000..003de0511 --- /dev/null +++ b/test/fixtures/custom-emoji-reaction.json @@ -0,0 +1,28 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "Hashtag": "as:Hashtag" + } + ], + "type": "Like", + "id": "https://misskey.local.live/likes/917ocsybgp", + "actor": "https://misskey.local.live/users/8x8yep20u2", + "object": "https://pleroma.local.live/objects/89937a53-2692-4631-bb62-770091267391", + "content": ":hanapog:", + "_misskey_reaction": ":hanapog:", + "tag": [ + { + "id": "https://misskey.local.live/emojis/hanapog", + "type": "Emoji", + "name": ":hanapog:", + "updated": "2022-06-07T12:00:05.773Z", + "icon": { + "type": "Image", + "mediaType": "image/png", + "url": "https://misskey.local.live/files/webpublic-8f8a9768-7264-4171-88d6-2356aabeadcd" + } + } + ] +} diff --git a/test/fixtures/fep-e232.json b/test/fixtures/fep-e232.json new file mode 100644 index 000000000..e9d12ae35 --- /dev/null +++ b/test/fixtures/fep-e232.json @@ -0,0 +1,31 @@ +{ + "@context": "https://www.w3.org/ns/activitystreams", + "type": "Create", + "actor": "https://example.org/users/alice", + "object": { + "id": "https://example.org/objects/10", + "type": "Note", + "attributedTo": "https://example.org/users/alice", + "content": "<p>test <a href=\"https://example.org/objects/9\">https://example.org/objects/9</a></p>", + "published": "2022-10-01T21:30:05.211215Z", + "tag": [ + { + "name": "@bob@example.net", + "type": "Mention", + "href": "https://example.net/users/bob" + }, + { + "name": "https://example.org/objects/9", + "type": "Link", + "href": "https://example.org/objects/9", + "mediaType": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + } + ], + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://example.org/users/alice/followers" + ] + } +} diff --git a/test/fixtures/hubzilla-actor.json b/test/fixtures/hubzilla-actor.json new file mode 100644 index 000000000..445d6413c --- /dev/null +++ b/test/fixtures/hubzilla-actor.json @@ -0,0 +1 @@ +{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1","https://hub.somaton.com/apschema/v1.9"],"type":"Person","id":"https://hub.somaton.com/channel/testc6","preferredUsername":"testc6","name":"testc6 lala","updated":"2021-08-29T10:07:23Z","icon":{"type":"Image","mediaType":"image/png","updated":"2021-10-09T04:54:35Z","url":"https://hub.somaton.com/photo/profile/l/33","height":300,"width":300},"url":"https://hub.somaton.com/channel/testc6","publicKey":{"id":"https://hub.somaton.com/channel/testc6","owner":"https://hub.somaton.com/channel/testc6","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq5ep+6MhhaAiqZSd8nXe\nUAokXNgqTr/DjUic5VDudjQgvetchaiBUieBnqpJSPNNAvvf6Qs4eDW4w2JQeA6y\nqEplKrmb8l1EyhwXeFLDUGQdf0f6hg++x5mIrO6uX0tlQGU6nutvhItn6JMZc5GU\nv3C/UW0OfHCCdHSGZ/1nIqq1P98FqF0+PA1pvTHCkLr4kcKzfpmkLjsccUSq0FGh\nQF+paW9FU89o4hkaH/X3E/Ac7DL8zgcyt29KSj4eUIvjBIEPAMdRno345fiZ+QYr\nlYQYaBC2gvozjxtxl9MyfqjBRzfl9VDHzoDvMn5+LD5dCRB1zOESv/b3EpiHYqXl\nwiPzP9az8e8cw6D72n/Mlrf27yIuVAdwaGdbAwekjIQZHIDoP0XNnA5i31RLpEMI\nbNpH47ChtjxeilQZ3va6qIShYfGlndpy/rx4i4Yt4xIG+BbGb/dWo3AbtHi64fPZ\nMoLuR71sEBe7uAvalJ+lopxuQ2qLJpCInukQ13p/G/n9tVDwbfGyumzr5hHk7JoY\nN+JqH737MCZqb9dRDof+fju58GY1VzFjBph38sHYJh0ykA+2BzYU2+nT7CDXfKWA\nsmHhizp7haoPjl/yclZG5FJwg3oqHTD14dASUs+OI4K+Q//74wfb4/6E3CDyOkW3\nUj+8TPZooKulxtQ9ezergr0CAwEAAQ==\n-----END PUBLIC KEY-----\n"},"outbox":"https://hub.somaton.com/outbox/testc6","inbox":"https://hub.somaton.com/inbox/testc6","followers":"https://hub.somaton.com/followers/testc6","following":"https://hub.somaton.com/following/testc6","endpoints":{"sharedInbox":"https://hub.somaton.com/inbox"},"discoverable":false,"signature":{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],"type":"RsaSignature2017","nonce":"8d6dea03f04cbb7faaf43958a4cf39a115ff1c61c7febaa6154c463eab9a42c8","creator":"https://hub.somaton.com/channel/testc6","created":"2021-10-13T18:21:48Z","signatureValue":"N4CJBO2K/8v7KI97REyJXaSYOlLWscuEDlODDnjNYD1fbVQFO3s2JtqPcN2lVJvNTlW5HUze+owaAYNcvZe3mNm1iz05Xru3s8yRA8bNCdKBuWd/3zb3/JQVkbSb09D2PloeuoKBQmPIn+dNiTyFR0jxLsxCXXTomGKigWPtTOUIt52Dv9MFJ3jRZmfoykT9bHrAIVCASHoiluhTkPAzc6pt0lSyZd0D3X4J1K4/sLXa8HRoooMFu2dHWfqV4tyLU9WzofAhvnYg9tEbKCH42DIAbwDfjAeC4qL8xkqAlYWLvXYVGH76cZLdp9Zuv1p3NHqaPEJ85MbuaUkfnU75Bx/Fcfoi0pEieWRdFvMx5b/UFwGbJd6iSAO1zRbGYTPEMPWHzh0AEAaLeyY+g3ZmpNu88ujrIr8iJ1U4EkjOBn8ooxA5LaI2fXDiYC2NwRiAbY+xVtgJgvHDi9tXCdvzjZWfU/cgiwF/cYMbsB2BCyPRd+XZhudfXSOysFC4WYnawhiRVevba9lQ6rEP4FMepOGq4ZOSGzxgw2xNIXpu0IkrxX5mEv/ahEhDy1KGRIFc0GnPJrv3kMVxJrZ7SF8PNAGqftQBLkqQR+SEygs3XB4cd2DQ2lPeiMd8+Xv+lBjtzZtZAM/Y4CZCOdV9DHXDGNSKKFDzzna4QcUzQ+KRc8w="}}
\ No newline at end of file diff --git a/test/fixtures/hubzilla-create-image.json b/test/fixtures/hubzilla-create-image.json new file mode 100644 index 000000000..9f0669bb7 --- /dev/null +++ b/test/fixtures/hubzilla-create-image.json @@ -0,0 +1 @@ +{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1","https://hub.somaton.com/apschema/v1.9"],"type":"Create","id":"https://hub.somaton.com/activity/452583b2-7e1f-4ac3-8334-ff666f134afe","diaspora:guid":"452583b2-7e1f-4ac3-8334-ff666f134afe","name":"daf82c18ef92a84cda72(1).jpg","published":"2021-10-12T21:28:26Z","actor":"https://hub.somaton.com/channel/testc6","object":{"type":"Image","name":"daf82c18ef92a84cda72(1).jpg","published":"2021-10-12T21:28:23Z","updated":"2021-10-12T21:28:23Z","attributedTo":"https://hub.somaton.com/channel/testc6","id":"https://hub.somaton.com/photo/452583b2-7e1f-4ac3-8334-ff666f134afe","url":[{"type":"Link","mediaType":"image/jpeg","href":"https://hub.somaton.com/photo/452583b2-7e1f-4ac3-8334-ff666f134afe-0.jpg","width":2200,"height":2200},{"type":"Link","mediaType":"image/jpeg","href":"https://hub.somaton.com/photo/452583b2-7e1f-4ac3-8334-ff666f134afe-1.jpg","width":1024,"height":1024},{"type":"Link","mediaType":"image/jpeg","href":"https://hub.somaton.com/photo/452583b2-7e1f-4ac3-8334-ff666f134afe-2.jpg","width":640,"height":640},{"type":"Link","mediaType":"image/jpeg","href":"https://hub.somaton.com/photo/452583b2-7e1f-4ac3-8334-ff666f134afe-3.jpg","width":320,"height":320},{"type":"Link","mediaType":"text/html","href":"https://hub.somaton.com/photos/testc6/image/452583b2-7e1f-4ac3-8334-ff666f134afe"}],"source":{"content":"[footer][zrl=https://hub.somaton.com/channel/testc6]testc6 lala[/zrl] posted [zrl=https://hub.somaton.com/photos/testc6/image/452583b2-7e1f-4ac3-8334-ff666f134afe]a new photo[/zrl] to [zrl=https://hub.somaton.com/photos/testc6/album/1e9b0d74-633e-4bd0-b37f-694bb0ed0145]test[/zrl][/footer]","mediaType":"text/bbcode"},"content":"<div class=\"wall-item-footer\"><a class=\"zrl\" href=\"https://hub.somaton.com/channel/testc6\" target=\"_blank\" rel=\"nofollow noopener\" >testc6 lala</a> posted <a class=\"zrl\" href=\"https://hub.somaton.com/photos/testc6/image/452583b2-7e1f-4ac3-8334-ff666f134afe\" target=\"_blank\" rel=\"nofollow noopener\" >a new photo</a> to <a class=\"zrl\" href=\"https://hub.somaton.com/photos/testc6/album/1e9b0d74-633e-4bd0-b37f-694bb0ed0145\" target=\"_blank\" rel=\"nofollow noopener\" >test</a></div>","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://hub.somaton.com/followers/testc6"]},"target":{"type":"orderedCollection","name":"test","id":"https://hub.somaton.com/album/testc6/test"},"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://hub.somaton.com/followers/testc6"],"signature":{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],"type":"RsaSignature2017","nonce":"e0d077edccf262f02ed59ff67e91a5324ccaffc3d2b3f23793b4bd24cdbe70bb","creator":"https://hub.somaton.com/channel/testc6","created":"2021-10-13T18:39:05Z","signatureValue":"YYU0/17PqqUmLCn4oVS2N62rV1G9WQ+wLax2cI+EpMw/WOWKuVvtGrvhzciQ5ITXoh3scrZRYH8Bke1jDWkjL9YtjVD6TjMsv6f3OoO1vvMNgEfQfgZJ78QQt5MoLrT2mkRa35lSmVHkTDROKJPrwIAnpN6bDb577wZ63BsuBjqW7ca/E6oXSIr+meCXv3kqkyYDSz0ImYvVmki+OfX97xbYkQlzM06EgK1LZTHfuf4sk09hVfDDqVB9tHO4ObYQCYNiOWRHjA5S1Cw8WX1OQJ+GCQ8yxHmtiU3tJsxeYhxGs7VEmTLUvf/QZ0VTPumkd1CewdxzNGvAP3f9JCakuV7eyk88oqF+p7xxfxmBjLYbMTuhrcZIdUdMcjW9pENOYBbt+a+FhPsjbm8zVU3iKPqe/8UAvo01hGW7jiKJUm4qdcX3H3MExTLEFuz0NTeqxl4djlyGTT9KBqNouD+/oSSgwm6qeRZ5y3RsC27N0HRbg74qNXhhWQZVWQtHdSCHjAfHVPOSpjxpSPs7qkMLQ0vPsVsCsukZz8JCoXRo+JoKuaiaRgfiIRGNBO/XEicKMyu2JCU+UmkroiDJHy+4IfZRevnlneRa1jmu5KA/4xk5KU8l0I0Inap7TSPhv14Ex2sF89LkT8MbcDM3S3QL4urYsQj37zOKRDTFzE96TmI="}}
\ No newline at end of file diff --git a/test/fixtures/image_with_imagedescription_and_caption-abstract_and_stray_data_after.png b/test/fixtures/image_with_imagedescription_and_caption-abstract_and_stray_data_after.png Binary files differnew file mode 100644 index 000000000..7ce8640fa --- /dev/null +++ b/test/fixtures/image_with_imagedescription_and_caption-abstract_and_stray_data_after.png diff --git a/test/fixtures/image_with_stray_data_after.png b/test/fixtures/image_with_stray_data_after.png Binary files differnew file mode 100755 index 000000000..a280e4377 --- /dev/null +++ b/test/fixtures/image_with_stray_data_after.png diff --git a/test/fixtures/mastodon-nodeinfo20.json b/test/fixtures/mastodon-nodeinfo20.json new file mode 100644 index 000000000..35010fdf0 --- /dev/null +++ b/test/fixtures/mastodon-nodeinfo20.json @@ -0,0 +1 @@ +{"version":"2.0","software":{"name":"mastodon","version":"4.1.0"},"protocols":["activitypub"],"services":{"outbound":[],"inbound":[]},"usage":{"users":{"total":971090,"activeMonth":167218,"activeHalfyear":384808},"localPosts":52071541},"openRegistrations":true,"metadata":{}}
\ No newline at end of file diff --git a/test/fixtures/mastodon-well-known-nodeinfo.json b/test/fixtures/mastodon-well-known-nodeinfo.json new file mode 100644 index 000000000..237d5462a --- /dev/null +++ b/test/fixtures/mastodon-well-known-nodeinfo.json @@ -0,0 +1 @@ +{"links":[{"rel":"http://nodeinfo.diaspora.software/ns/schema/2.0","href":"https://mastodon.example.org/nodeinfo/2.0"}]}
\ No newline at end of file diff --git a/test/fixtures/minds-invalid-mention-post.json b/test/fixtures/minds-invalid-mention-post.json new file mode 100644 index 000000000..ea2cb2739 --- /dev/null +++ b/test/fixtures/minds-invalid-mention-post.json @@ -0,0 +1 @@ +{"@context":"https://www.w3.org/ns/activitystreams","type":"Note","id":"https://www.minds.com/api/activitypub/users/1198929502760083472/entities/urn:comment:1600926863310458883:0:0:0:1600932467852709903","attributedTo":"https://www.minds.com/api/activitypub/users/1198929502760083472","content":"\u003Ca class=\u0022u-url mention\u0022 href=\u0022https://www.minds.com/lain\u0022 target=\u0022_blank\u0022\u003E@lain\u003C/a\u003E corn syrup.","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://www.minds.com/api/activitypub/users/1198929502760083472/followers","https://lain.com/users/lain"],"tag":[{"type":"Mention","href":"https://www.minds.com/api/activitypub/users/464237775479123984","name":"@lain"}],"url":"https://www.minds.com/newsfeed/1600926863310458883?focusedCommentUrn=urn:comment:1600926863310458883:0:0:0:1600932467852709903","published":"2024-02-04T17:34:03+00:00","inReplyTo":"https://lain.com/objects/36254095-c839-4167-bcc2-b361d5de9198","source":{"content":"@lain corn syrup.","mediaType":"text/plain"}}
\ No newline at end of file diff --git a/test/fixtures/minds-pleroma-mentioned-post.json b/test/fixtures/minds-pleroma-mentioned-post.json new file mode 100644 index 000000000..9dfa42c90 --- /dev/null +++ b/test/fixtures/minds-pleroma-mentioned-post.json @@ -0,0 +1 @@ +{"@context":["https://www.w3.org/ns/activitystreams","https://lain.com/schemas/litepub-0.1.jsonld",{"@language":"und"}],"actor":"https://lain.com/users/lain","attachment":[],"attributedTo":"https://lain.com/users/lain","cc":["https://lain.com/users/lain/followers"],"content":"which diet is the best for cognitive dissonance","context":"https://lain.com/contexts/98c8a130-e813-4797-8973-600e80114317","conversation":"https://lain.com/contexts/98c8a130-e813-4797-8973-600e80114317","id":"https://lain.com/objects/36254095-c839-4167-bcc2-b361d5de9198","published":"2024-02-04T17:11:23.931890Z","repliesCount":11,"sensitive":null,"source":{"content":"which diet is the best for cognitive dissonance","mediaType":"text/plain"},"summary":"","tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"type":"Note"}
\ No newline at end of file diff --git a/test/fixtures/owncast-note-with-attachment.json b/test/fixtures/owncast-note-with-attachment.json new file mode 100644 index 000000000..68cb6bbf7 --- /dev/null +++ b/test/fixtures/owncast-note-with-attachment.json @@ -0,0 +1,31 @@ +{ + "attachment": { + "content": "Live stream preview", + "type": "Image", + "url": "https://owncast.localhost.localdomain/preview.gif?us=KjfNX387gm" + }, + "attributedTo": "https://owncast.localhost.localdomain/federation/user/streamer", + "audience": "https://www.w3.org/ns/activitystreams#Public", + "content": "<p>I've gone live!</p><p></p><p><a class=\"hashtag\" href=\"https://directory.owncast.online/tags/owncast\">#owncast</a> <a class=\"hashtag\" href=\"https://directory.owncast.online/tags/streaming\">#streaming</a></p><a href=\"https://owncast.localhost.localdomain\">https://owncast.localhost.localdomain</a>", + "id": "https://owncast.localhost.localdomain/federation/KjBNuq8ng", + "published": "2022-04-17T15:42:03Z", + "tag": [ + { + "href": "https://directory.owncast.online/tags/owncast", + "name": "#owncast", + "type": "Hashtag" + }, + { + "href": "https://directory.owncast.online/tags/streaming", + "name": "#streaming", + "type": "Hashtag" + }, + { + "href": "https://directory.owncast.online/tags/owncast", + "name": "#owncast", + "type": "Hashtag" + } + ], + "to": "https://www.w3.org/ns/activitystreams#Public", + "type": "Note" +} diff --git a/test/fixtures/png_with_transparency.png b/test/fixtures/png_with_transparency.png Binary files differnew file mode 100644 index 000000000..7963149db --- /dev/null +++ b/test/fixtures/png_with_transparency.png diff --git a/test/fixtures/quote_post/fedibird_quote_mismatched.json b/test/fixtures/quote_post/fedibird_quote_mismatched.json new file mode 100644 index 000000000..8dee5daff --- /dev/null +++ b/test/fixtures/quote_post/fedibird_quote_mismatched.json @@ -0,0 +1,54 @@ +{ + "@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", + "fedibird": "http://fedibird.com/ns#", + "quoteUri": "fedibird:quoteUri", + "expiry": "fedibird:expiry" + } + ], + "id": "https://fedibird.com/users/noellabo/statuses/107712183700212249", + "type": "Note", + "summary": null, + "inReplyTo": null, + "published": "2022-01-30T15:44:50Z", + "url": "https://fedibird.com/@noellabo/107712183700212249", + "attributedTo": "https://fedibird.com/users/noellabo", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://fedibird.com/users/noellabo/followers" + ], + "sensitive": false, + "atomUri": "https://fedibird.com/users/noellabo/statuses/107712183700212249", + "inReplyToAtomUri": null, + "conversation": "tag:fedibird.com,2022-01-30:objectId=107712183700170473:objectType=Conversation", + "context": "https://fedibird.com/contexts/107712183700170473", + "quoteUri": "https://unnerv.jp/users/UN_NERV/statuses/107712176849067434", + "_misskey_quote": "https://unnerv.jp/users/UN_NERV/statuses/107712176849067434", + "_misskey_content": "揺れていたようだ", + "content": "<p>揺れていたようだ<span class=\"quote-inline\"><br/>QT: <a class=\"status-url-link\" data-status-account-acct=\"UN_NERV@unnerv.jp\" data-status-id=\"107712177062934465\" href=\"https://unnerv.jp/@UN_NERV/107712176849067434\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">unnerv.jp/@UN_NERV/10771217684</span><span class=\"invisible\">9067434</span></a></span></p>", + "contentMap": { + "ja": "<p>揺れていたようだ<span class=\"quote-inline\"><br/>QT: <a class=\"status-url-link\" data-status-account-acct=\"UN_NERV@unnerv.jp\" data-status-id=\"107712177062934465\" href=\"https://unnerv.jp/@UN_NERV/107712176849067434\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">unnerv.jp/@UN_NERV/10771217684</span><span class=\"invisible\">9067434</span></a></span></p>" + }, + "attachment": [], + "tag": [], + "replies": { + "id": "https://fedibird.com/users/noellabo/statuses/107712183700212249/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://fedibird.com/users/noellabo/statuses/107712183700212249/replies?only_other_accounts=true&page=true", + "partOf": "https://fedibird.com/users/noellabo/statuses/107712183700212249/replies", + "items": [] + } + } +} diff --git a/test/fixtures/quote_post/fedibird_quote_post.json b/test/fixtures/quote_post/fedibird_quote_post.json new file mode 100644 index 000000000..ebf383356 --- /dev/null +++ b/test/fixtures/quote_post/fedibird_quote_post.json @@ -0,0 +1,52 @@ +{ + "@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", + "expiry": "toot:expiry" + } + ], + "id": "https://fedibird.com/users/noellabo/statuses/107663670404015196", + "type": "Note", + "summary": null, + "inReplyTo": null, + "published": "2022-01-22T02:07:16Z", + "url": "https://fedibird.com/@noellabo/107663670404015196", + "attributedTo": "https://fedibird.com/users/noellabo", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://fedibird.com/users/noellabo/followers" + ], + "sensitive": false, + "atomUri": "https://fedibird.com/users/noellabo/statuses/107663670404015196", + "inReplyToAtomUri": null, + "conversation": "tag:fedibird.com,2022-01-22:objectId=107663670404038002:objectType=Conversation", + "context": "https://fedibird.com/contexts/107663670404038002", + "quoteURL": "https://misskey.io/notes/8vsn2izjwh", + "_misskey_quote": "https://misskey.io/notes/8vsn2izjwh", + "_misskey_content": "いつの生まれだシトリン", + "content": "<p>いつの生まれだシトリン<span class=\"quote-inline\"><br/>QT: <a class=\"status-url-link\" data-status-account-acct=\"Citrine@misskey.io\" data-status-id=\"107663207194225003\" href=\"https://misskey.io/notes/8vsn2izjwh\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">misskey.io/notes/8vsn2izjwh</span><span class=\"invisible\"></span></a></span></p>", + "contentMap": { + "ja": "<p>いつの生まれだシトリン<span class=\"quote-inline\"><br/>QT: <a class=\"status-url-link\" data-status-account-acct=\"Citrine@misskey.io\" data-status-id=\"107663207194225003\" href=\"https://misskey.io/notes/8vsn2izjwh\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">misskey.io/notes/8vsn2izjwh</span><span class=\"invisible\"></span></a></span></p>" + }, + "attachment": [], + "tag": [], + "replies": { + "id": "https://fedibird.com/users/noellabo/statuses/107663670404015196/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://fedibird.com/users/noellabo/statuses/107663670404015196/replies?only_other_accounts=true&page=true", + "partOf": "https://fedibird.com/users/noellabo/statuses/107663670404015196/replies", + "items": [] + } + } +} diff --git a/test/fixtures/quote_post/fedibird_quote_uri.json b/test/fixtures/quote_post/fedibird_quote_uri.json new file mode 100644 index 000000000..7c328fdb9 --- /dev/null +++ b/test/fixtures/quote_post/fedibird_quote_uri.json @@ -0,0 +1,54 @@ +{ + "@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", + "fedibird": "http://fedibird.com/ns#", + "quoteUri": "fedibird:quoteUri", + "expiry": "fedibird:expiry" + } + ], + "id": "https://fedibird.com/users/noellabo/statuses/107699335988346142", + "type": "Note", + "summary": null, + "inReplyTo": null, + "published": "2022-01-28T09:17:30Z", + "url": "https://fedibird.com/@noellabo/107699335988346142", + "attributedTo": "https://fedibird.com/users/noellabo", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://fedibird.com/users/noellabo/followers" + ], + "sensitive": false, + "atomUri": "https://fedibird.com/users/noellabo/statuses/107699335988346142", + "inReplyToAtomUri": null, + "conversation": "tag:fedibird.com,2022-01-28:objectId=107699335988345290:objectType=Conversation", + "context": "https://fedibird.com/contexts/107699335988345290", + "quoteUri": "https://fedibird.com/users/yamako/statuses/107699333438289729", + "_misskey_quote": "https://fedibird.com/users/yamako/statuses/107699333438289729", + "_misskey_content": "美味しそう", + "content": "<p>美味しそう<span class=\"quote-inline\"><br/>QT: <a class=\"status-url-link\" data-status-account-acct=\"yamako\" data-status-id=\"107699333438289729\" href=\"https://fedibird.com/@yamako/107699333438289729\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">fedibird.com/@yamako/107699333</span><span class=\"invisible\">438289729</span></a></span></p>", + "contentMap": { + "ja": "<p>美味しそう<span class=\"quote-inline\"><br/>QT: <a class=\"status-url-link\" data-status-account-acct=\"yamako\" data-status-id=\"107699333438289729\" href=\"https://fedibird.com/@yamako/107699333438289729\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">fedibird.com/@yamako/107699333</span><span class=\"invisible\">438289729</span></a></span></p>" + }, + "attachment": [], + "tag": [], + "replies": { + "id": "https://fedibird.com/users/noellabo/statuses/107699335988346142/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://fedibird.com/users/noellabo/statuses/107699335988346142/replies?only_other_accounts=true&page=true", + "partOf": "https://fedibird.com/users/noellabo/statuses/107699335988346142/replies", + "items": [] + } + } +} diff --git a/test/fixtures/quote_post/fep-e232-tag-example.json b/test/fixtures/quote_post/fep-e232-tag-example.json new file mode 100644 index 000000000..23c7fb5ac --- /dev/null +++ b/test/fixtures/quote_post/fep-e232-tag-example.json @@ -0,0 +1,17 @@ +{ + "@context": "https://www.w3.org/ns/activitystreams", + "type": "Note", + "content": "This is a quote:<br>RE: https://server.example/objects/123", + "tag": [ + { + "type": "Link", + "mediaType": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", + "href": "https://server.example/objects/123", + "name": "RE: https://server.example/objects/123" + } + ], + "id": "https://server.example/objects/1", + "to": "https://server.example/users/1", + "attributedTo": "https://server.example/users/1", + "actor": "https://server.example/users/1" +} diff --git a/test/fixtures/quote_post/misskey_quote_post.json b/test/fixtures/quote_post/misskey_quote_post.json new file mode 100644 index 000000000..59f677ca9 --- /dev/null +++ b/test/fixtures/quote_post/misskey_quote_post.json @@ -0,0 +1,46 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "Hashtag": "as:Hashtag", + "quoteUrl": "as:quoteUrl", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "featured": "toot:featured", + "discoverable": "toot:discoverable", + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "misskey": "https://misskey.io/ns#", + "_misskey_content": "misskey:_misskey_content", + "_misskey_quote": "misskey:_misskey_quote", + "_misskey_reaction": "misskey:_misskey_reaction", + "_misskey_votes": "misskey:_misskey_votes", + "_misskey_talk": "misskey:_misskey_talk", + "isCat": "misskey:isCat", + "vcard": "http://www.w3.org/2006/vcard/ns#" + } + ], + "id": "https://misskey.io/notes/8vs6ylpfez", + "type": "Note", + "attributedTo": "https://misskey.io/users/7rkrarq81i", + "summary": null, + "content": "<p><span>投稿者の設定によるね<br>Fanboxについても投稿者によっては過去の投稿は高額なプランに移動してることがある<br><br>RE: </span><a href=\"https://misskey.io/notes/8vs6wxufd0\">https://misskey.io/notes/8vs6wxufd0</a></p>", + "_misskey_content": "投稿者の設定によるね\nFanboxについても投稿者によっては過去の投稿は高額なプランに移動してることがある", + "_misskey_quote": "https://misskey.io/notes/8vs6wxufd0", + "quoteUrl": "https://misskey.io/notes/8vs6wxufd0", + "published": "2022-01-21T16:38:30.243Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://misskey.io/users/7rkrarq81i/followers" + ], + "inReplyTo": null, + "attachment": [], + "sensitive": false, + "tag": [] +} diff --git a/test/fixtures/rich_media/google.html b/test/fixtures/rich_media/google.html new file mode 100644 index 000000000..c068397a5 --- /dev/null +++ b/test/fixtures/rich_media/google.html @@ -0,0 +1,12 @@ +<meta property="og:url" content="https://google.com"> +<meta property="og:type" content="website"> +<meta property="og:title" content="Google"> +<meta property="og:description" content="Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for."> +<meta property="og:image" content=""> + +<meta name="twitter:card" content="summary_large_image"> +<meta property="twitter:domain" content="google.com"> +<meta property="twitter:url" content="https://google.com"> +<meta name="twitter:title" content="Google"> +<meta name="twitter:description" content="Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for."> +<meta name="twitter:image" content=""> diff --git a/test/fixtures/rich_media/oembed.html b/test/fixtures/rich_media/oembed.html index 55f17004b..5429630d0 100644 --- a/test/fixtures/rich_media/oembed.html +++ b/test/fixtures/rich_media/oembed.html @@ -1,3 +1,3 @@ <link rel="alternate" type="application/json+oembed" - href="http://example.com/oembed.json" + href="https://example.com/oembed.json" title="Bacon Lollys oEmbed Profile" /> diff --git a/test/fixtures/rich_media/reddit.html b/test/fixtures/rich_media/reddit.html new file mode 100644 index 000000000..a99bb6884 --- /dev/null +++ b/test/fixtures/rich_media/reddit.html @@ -0,0 +1,392 @@ +<!doctype html><html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"><head><title>Twitter/X is getting weirder; where now for security news and analysis? : cybersecurity</title><meta name="keywords" content=" reddit, reddit.com, vote, comment, submit " /><meta name="description" content="I primarily use Twitter/X as a glorified RSS feed of security news and analysis reporting. Lately its getting heavier on ads and weird pushed..." /><meta name="referrer" content="always"><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><link type="application/opensearchdescription+xml" rel="search" href="/static/opensearch.xml"/><link rel="canonical" href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" /><link rel="amphtml" href="https://amp.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" /><meta name="viewport" content="width=1024"><link rel="shorturl" href="https://redd.it/16nf2ev"/><link rel="dns-prefetch" href="//out.reddit.com"><link rel="preconnect" href="//out.reddit.com"><meta property="og:image" content="https://www.redditstatic.com/new-icon.png"><meta property="og:ttl" content="600"><meta property="og:site_name" content="reddit"><meta property="og:description" content="I primarily use Twitter/X as a glorified RSS feed of security news and analysis reporting. Lately its getting heavier on ads and weird pushed..."><meta property="og:title" content="Twitter/X is getting weirder; where now for security news and analysis?"><meta property="al:android:package" content="com.reddit.frontpage"><meta property="al:ios:app_name" content="Reddit"><meta property="al:ios:url" content="reddit://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/"><meta property="al:ios:app_store_id" content="1064216828"><meta property="twitter:site" content="@reddit"><meta property="twitter:card" content="summary"><meta property="twitter:title" content="Twitter/X is getting weirder; where now for security news and..."><link rel="apple-touch-icon" sizes="57x57" href="//www.redditstatic.com/desktop2x/img/favicon/apple-icon-57x57.png" /><link rel="apple-touch-icon" sizes="60x60" href="//www.redditstatic.com/desktop2x/img/favicon/apple-icon-60x60.png" /><link rel="apple-touch-icon" sizes="72x72" href="//www.redditstatic.com/desktop2x/img/favicon/apple-icon-72x72.png" /><link rel="apple-touch-icon" sizes="76x76" href="//www.redditstatic.com/desktop2x/img/favicon/apple-icon-76x76.png" /><link rel="apple-touch-icon" sizes="114x114" href="//www.redditstatic.com/desktop2x/img/favicon/apple-icon-114x114.png" /><link rel="apple-touch-icon" sizes="120x120" href="//www.redditstatic.com/desktop2x/img/favicon/apple-icon-120x120.png" /><link rel="apple-touch-icon" sizes="144x144" href="//www.redditstatic.com/desktop2x/img/favicon/apple-icon-144x144.png" /><link rel="apple-touch-icon" sizes="152x152" href="//www.redditstatic.com/desktop2x/img/favicon/apple-icon-152x152.png" /><link rel="apple-touch-icon" sizes="180x180" href="//www.redditstatic.com/desktop2x/img/favicon/apple-icon-180x180.png" /><link rel="icon" type="image/png" sizes="192x192" href="//www.redditstatic.com/desktop2x/img/favicon/android-icon-192x192.png" /><link rel="icon" type="image/png" sizes="32x32" href="//www.redditstatic.com/desktop2x/img/favicon/favicon-32x32.png" /><link rel="icon" type="image/png" sizes="96x96" href="//www.redditstatic.com/desktop2x/img/favicon/favicon-96x96.png" /><link rel="icon" type="image/png" sizes="16x16" href="//www.redditstatic.com/desktop2x/img/favicon/favicon-16x16.png" /><link rel="manifest" href="//www.redditstatic.com/desktop2x/img/favicon/manifest.json"/><meta name="msapplication-TileColor" content="#ffffff"/><meta name="msapplication-TileImage" content="//www.redditstatic.com/desktop2x/img/favicon/ms-icon-144x144.png"/><meta name="theme-color" content="#ffffff"/><link rel="alternate" type="application/atom+xml" title="RSS" href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/.rss" /><link rel="stylesheet" type="text/css" href="//www.redditstatic.com/reddit.YXox_dqXzrc.css" media="all"><link rel="stylesheet" type="text/css" href="//www.redditstatic.com/expando.gMzRK16vwrQ.css" media="all"><link rel="stylesheet" type="text/css" href="//www.redditstatic.com/crosspost-preview.De3P20Yb4PY.css" media="all"><link rel="stylesheet" type="text/css" href="//www.redditstatic.com/author-tooltip.1VKQhhDIRMI.css" media="all"><link rel="stylesheet" type="text/css" href="//www.redditstatic.com/listing-comments.AZZO7Kj_O88.css" media="all"><link rel="stylesheet" type="text/css" href="//www.redditstatic.com/popup-notification.6-JvPBpHWMo.css" media="all"><link rel="stylesheet" type="text/css" href="//www.redditstatic.com/about-this-ad-modal.zVecmeeCuWY.css" media="all"><link rel="stylesheet" type="text/css" href="//www.redditstatic.com/desktoponboarding.k2RNrAG42v4.css" media="all"><link rel="stylesheet" type="text/css" href="//www.redditstatic.com/videoplayer.ANmi3DZjWG4.css" media="all"><link rel="stylesheet" type="text/css" href="//www.redditstatic.com/videoplayercontrols.a_TwaTy76-k.css" media="all"><!--[if gte IE 8]><!--><link rel="stylesheet" href="https://b.thumbs.redditmedia.com/ba_fm376ctS_mGlZGabqddmkhth3jqnccUyhKW7iGBo.css" ref="applied_subreddit_stylesheet" title="applied_subreddit_stylesheet" type="text/css"><!--<![endif]--><!--[if gte IE 9]><!--><script type="text/javascript" src="//www.redditstatic.com/reddit-init.en.a6Ar54Z0rBo.js"></script><!--<![endif]--><!--[if lt IE 9]><script type="text/javascript" src="//www.redditstatic.com/reddit-init-legacy.en.sAcI0xGFcwg.js"></script><![endif]--><script type="text/javascript" src="//www.redditstatic.com/videoplayer.XCrwE8Bi5A4.js"></script><script type="text/javascript" id="config">r.setup({"ajax_domain": "www.reddit.com", "post_site": "cybersecurity", "gold": false, "scraped_image_extensions": ["gif", "jpeg", "jpg", "png", "tiff"], "poisoning_report_mac": null, "requires_eu_cookie_policy": false, "nsfw_media_acknowledged": false, "stats_domain": "", "feature_net_neutrality": false, "cur_screen_name": "", "country_code": "", "facebook_app_id": "322647334569188", "loid": "000000000uhuuh3cqp", "is_sponsor": false, "has_gold_subscription": false, "feature_author_tooltip_users": true, "user_id": false, "pref_email_messages": false, "poisoning_canary": null, "logged": false, "over_18": false, "show_archived_signup_cta": true, "loid_created": 1708312402583, "mweb_blacklist_expressions": ["^/prefs/?", "^/live/?", "/message/compose", "/m/", "^/subreddits/create", "^/gold", "^/advertising", "^/promoted", "^/buttons"], "feature_noreferrer_to_noopener": true, "is_posts_mod": false, "modhash": "", "external_frame": false, "feature_cookie_consent_banner": true, "send_logs": true, "user_subscription_size": 0, "listing_over_18": false, "https_endpoint": "https://www.reddit.com", "extension": null, "embedded": false, "event_target": {"target_id": 2578913527, "target_type": "self", "target_sort": "confidence", "target_fullname": "t3_16nf2ev"}, "use_onetrust": false, "ads_loading_timeout_ms": 5000, "enabled_experiments": {}, "cache_policy": "loggedout_www", "admin_message_acct": "/r/reddit.com", "hidden_post_card_enabled": false, "advertiser_category": null, "events_collector_v2_url": "https://www.reddit.com/api/vote", "debug": false, "has_subscribed": false, "static_root": "//www.redditstatic.com", "server_time": 1708312403.0, "feature_no_subscription_step": "control_1", "feature_swap_steps_two_and_three_recalibration": "control_2", "pref_no_profanity": true, "share_tracking_ts": 1708312403232, "cur_domain": "reddit.com", "browser_supports_d2x": false, "events_collector_url": "https://www.reddit.com/api/vote", "embed_preview_url": "https://rebed.redditmedia.com/embed", "gild_url": "/framedGild", "cur_link": "t3_16nf2ev", "user_in_timeout": false, "share_tracking_hmac": null, "live_orangereds_pref": true, "feature_blocked_user_enabled": false, "ad_serving_events_sample_rate": "1.00", "is_fake": false, "renderstyle": "html", "framed_modal_url": "/framedModal/", "feature_adblock_v2_events_enabled": false, "user_age": false, "vote_hash": "4piNiL72kb0vN4fiRASdIAWORVe57zU0rsH8oo7AucKTKbAJQLfbHHo5RHL3/TKQx1yuwQbqBTpkfx1e/jhijKl6RZROrrFN0hJLyzXBCO0hF9IabySW12sBeNpMhTkrRffaXEgdyVj6LxzXZ2XAHPZPz5b7yVHbKOdgXhJ2ENI=", "link_websocket_url": "", "events_collector_secret": "Ua3epahc7ZiengeeVaeG6eingahke7", "pref_video_autoplay": true, "events_collector_key": "RedditFrontend3", "allow_nonessential_cookies": false, "scraped_domains": ["gfycat.com", "imgur.com"], "expando_preference": "subreddit_default", "store_visits": false, "onetrust_client_id": "14003311-a669-490b-a682-56294eb02bf2", "cur_site": "t5_2u559", "new_window": false, "pref_beta": false, "channels_mod_permissions_enabled": true, "eu_cookie_max_attempts": 3, "pageInfo": {"actionName": "front.GET_comments", "statsVerification": "388228f7328f4f4378f87784f3f47137498db25f", "type": "post_detail", "verification": "388228f7328f4f4378f87784f3f47137498db25f", "statsName": "front.GET_comments"}, "user_websocket_url": null, "signature_header": "X-Signature", "media_domain": "www.redditmedia.com", "whitelist_status": "all_ads", "signature_header_v2": "X-Signature-v2", "cur_listing": "cybersecurity", "email_verified": false, "status_msg": {"fetching": "fetching title...", "loading": "loading...", "submitting": "submitting..."}, "link_limit": 200, "stats_sample_rate": "0", "loid_version": 2, "eu_cookie": "eu_cookie", "is_moderator_somewhere": false, "d2x_domain": "https://new.reddit.com/"})</script><style type="text/css">/* Custom css: use this block to insert special translation-dependent css in the page header */</style></head><body class="single-page comments-page" ><script>var frame = document.createElement('iframe'); frame.style.display = 'none'; frame.referrer = 'no-referrer'; frame.id = 'gtm-jail'; frame.name = JSON.stringify({ subreddit: r.config.post_site, origin: location.origin, url: location.href, userMatching: false, userId: r.config.user_id, advertiserCategory: r.config.advertiser_category, adsStatus: r.config.whitelist_status, }); frame.src = '//' + "www.redditmedia.com" + '/gtm/jail?cb=' + "8CqR7FcToPI"; document.body.appendChild(frame);</script><div id="header" role="banner"><a tabindex="1" href="#content" id="jumpToContent">jump to content</a><div id="sr-header-area"><div class="width-clip"><div class="dropdown srdrop" onclick="open_menu(this)"><span class="selected title">my subreddits</span></div><div class="drop-choices srdrop"><a href="https://www.reddit.com/subreddits/" class="bottom-option choice" >edit subscriptions</a></div><div class="sr-list"><ul class="flat-list sr-bar hover" ><li ><a href="https://www.reddit.com/r/popular/" class="choice" >popular</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/all/" class="choice" >all</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/random/" class="random choice" >random</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/users/" class="choice" >users</a></li></ul><span class="separator"> | </span><ul class="flat-list sr-bar hover" id='sr-bar'><li ><a href="https://www.reddit.com/r/AskReddit/" class="choice" >AskReddit</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/pics/" class="choice" >pics</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/funny/" class="choice" >funny</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/movies/" class="choice" >movies</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/gaming/" class="choice" >gaming</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/worldnews/" class="choice" >worldnews</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/news/" class="choice" >news</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/todayilearned/" class="choice" >todayilearned</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/nottheonion/" class="choice" >nottheonion</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/explainlikeimfive/" class="choice" >explainlikeimfive</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/mildlyinteresting/" class="choice" >mildlyinteresting</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/DIY/" class="choice" >DIY</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/videos/" class="choice" >videos</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/OldSchoolCool/" class="choice" >OldSchoolCool</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/TwoXChromosomes/" class="choice" >TwoXChromosomes</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/tifu/" class="choice" >tifu</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/Music/" class="choice" >Music</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/books/" class="choice" >books</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/LifeProTips/" class="choice" >LifeProTips</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/dataisbeautiful/" class="choice" >dataisbeautiful</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/aww/" class="choice" >aww</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/science/" class="choice" >science</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/space/" class="choice" >space</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/Showerthoughts/" class="choice" >Showerthoughts</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/askscience/" class="choice" >askscience</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/Jokes/" class="choice" >Jokes</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/IAmA/" class="choice" >IAmA</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/Futurology/" class="choice" >Futurology</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/sports/" class="choice" >sports</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/UpliftingNews/" class="choice" >UpliftingNews</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/food/" class="choice" >food</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/nosleep/" class="choice" >nosleep</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/creepy/" class="choice" >creepy</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/history/" class="choice" >history</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/gifs/" class="choice" >gifs</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/InternetIsBeautiful/" class="choice" >InternetIsBeautiful</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/GetMotivated/" class="choice" >GetMotivated</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/gadgets/" class="choice" >gadgets</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/announcements/" class="choice" >announcements</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/WritingPrompts/" class="choice" >WritingPrompts</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/philosophy/" class="choice" >philosophy</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/Documentaries/" class="choice" >Documentaries</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/EarthPorn/" class="choice" >EarthPorn</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/photoshopbattles/" class="choice" >photoshopbattles</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/listentothis/" class="choice" >listentothis</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/blog/" class="choice" >blog</a></li></ul></div><a href="https://www.reddit.com/subreddits/" id="sr-more-link" >more »</a></div></div><div id="header-bottom-left"><a href="/" id="header-img" class="default-header" title="">reddit.com</a> <span class="hover pagename redditname"><a href="https://www.reddit.com/r/cybersecurity/" >cybersecurity</a></span><ul class="tabmenu " ><li class='selected'><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" class="choice" >comments</a></li></ul></div><div id="header-bottom-right"><span class="user">Want to join? <a href="https://www.reddit.com/login" class="login-required login-link" >Log in</a> or <a href="https://www.reddit.com/login" class="login-required" >sign up</a> in seconds.</span><span class="separator">|</span><ul class="flat-list hover" ><li ><a href="javascript:void(0)" class="pref-lang choice" onclick="return showlang();" >English</a></li></ul></div></div><div class="side"><div class='spacer'><form action="https://www.reddit.com/r/cybersecurity/search" id="search" role="search"><input type="text" name="q" placeholder="search" tabindex="20"><input type="submit" value="" tabindex="22"><div id="searchexpando" class="infobar"><label><input type="checkbox" name="restrict_sr" tabindex="21">limit my search to r/cybersecurity</label><div id="moresearchinfo"><p>use the following search parameters to narrow your results:</p><dl><dt>subreddit:<i>subreddit</i></dt><dd>find submissions in "subreddit"</dd><dt>author:<i>username</i></dt><dd>find submissions by "username"</dd><dt>site:<i>example.com</i></dt><dd>find submissions from "example.com"</dd><dt>url:<i>text</i></dt><dd>search for "text" in url</dd><dt>selftext:<i>text</i></dt><dd>search for "text" in self post contents</dd><dt>self:yes (or self:no)</dt><dd>include (or exclude) self posts</dd><dt>nsfw:yes (or nsfw:no)</dt><dd>include (or exclude) results marked as NSFW</dd></dl><p>e.g. <code>subreddit:aww site:imgur.com dog</code></p><p><a href="https://www.reddit.com/wiki/search">see the search faq for details.</a></p></div><p><a href="https://www.reddit.com/wiki/search" id="search_showmore">advanced search: by author, subreddit...</a></p></div></form></div><div class='spacer'><div class="linkinfo"><div class="date"><span>this post was submitted on  </span><time datetime="2023-09-20T07:34:36+00:00">20 Sep 2023</time></div><div class="score"><span class="number">241</span> <span class="word">points</span> (92% upvoted)</div><div class="shortlink">shortlink:  <input type="text" value="https://redd.it/16nf2ev" readonly="readonly" id="shortlink-text" /></div></div></div><div class='spacer'><form method="post" action="https://www.reddit.com/r/cybersecurity/post/login" id="login_login-main" class="login-form login-form-side"><input type="hidden" name="op" value="login-main" /><input name="user" placeholder="username" type="text" maxlength="20" tabindex="1"/><input name="passwd" placeholder="password" type="password" tabindex="1"/><div class="g-recaptcha" data-sitekey="6LeTnxkTAAAAAN9QEuDZRpn90WwKk_R1TRW_g-JC"></div><div class="status"></div><div id="remember-me"><input type="checkbox" name="rem" id="rem-login-main" tabindex="1" /><label for="rem-login-main">remember me</label><a class="recover-password" href="/password">reset password</a></div><div class="submit"><span class="throbber"></span><button class="btn" type="submit" tabindex="1">login</button></div><div class="clear"></div></form></div><div class='spacer'></div><div class='spacer'><div class="sidebox submit submit-link"><div class="morelink"><a href="https://www.reddit.com/r/cybersecurity/submit" data-event-action="submit" data-type="subreddit" data-event-detail="link" class="login-required access-required" target="_top" >Submit a new link</a><div class="nub"></div></div></div></div><div class='spacer'><div class="sidebox submit submit-text"><div class="morelink"><a href="https://www.reddit.com/r/cybersecurity/submit?selftext=true" data-event-action="submit" data-type="subreddit" data-event-detail="self" class="login-required access-required" target="_top" >Submit a new text post</a><div class="nub"></div></div></div></div><div class='spacer'><a href="/premium" alt="get premium" class="premium-banner-outer"><form action="/premium" class="premium-banner"><div class="premium-banner__logo"></div><div class="premium-banner__title">Get an ad-free experience with special benefits, and directly support Reddit.</div><button class="premium-banner__button">get reddit premium</button></form></a></div><div class='spacer'><div class="titlebox"><h1 class="hover redditname"><a href="https://www.reddit.com/r/cybersecurity/" class="hover" >cybersecurity</a></h1><span class="fancy-toggle-button subscribe-button toggle" style="" data-sr_name="cybersecurity" ><a class="option active add login-required" href="#" tabindex="100" >join</a><a class="option remove" href="#">leave</a></span><span class="subscribers"><span class="number">688,076</span> <span class="word">readers</span></span><p class="users-online" title="users viewing this subreddit in the past 15 minutes"><span class="number">592</span> <span class="word">users here now</span></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t5_2u559nsa"><input type="hidden" name="thing_id" value="t5_2u559"/><div class="usertext-body may-blank-within md-container " ><div class="md"><h1><strong>NOTICE:</strong></h1> + +<h1><strong>This sidebar and rules are no longer being updated. To see the current sidebar and rules you must view them on new reddit.</strong></h1> + +<p><a href="https://new.reddit.com/r/cybersecurity">https://new.reddit.com/r/cybersecurity</a></p> + +<p>This security forum is oriented towards private white hat security professionals. Please note, the 'old' Reddit is no longer kept up to date. Please use 'new' Reddit. </p> + +<ol> +<li><p><strong>No Low Effort / Poor Quality Posts</strong> +"This security forum is oriented towards private white hat security professionals." If a post has very basic information, it is not appropriate for this sub. For example, "why passwords are important" is too fundamental.</p></li> +<li><p><strong>Must be relevant to security professionals</strong> +This is not a general security subreddit. Posts related to burglar alarms, weapons, and similar concepts are not appropriate for this sub.</p></li> +<li><p><strong>No fundamental security questions or tech support requests</strong> +Basic questions on security concepts and fundamentals and requests for tech support are not appropriate for this subreddit.</p></li> +<li><p><strong>SECURITY FIRST (no editorializing)</strong> +This is the guiding principle for all posts. No editorializing and no political agendas. Posts discussing political issues that affect security are fine, but the post must be geared towards the security implication. Such posts will be heavily monitored and comments may be locked as needed.</p></li> +<li><p><strong>Civility</strong> +We're all professionals. Be excellent to each other.</p></li> +<li><p><strong>No Advertising</strong> +Want to share information or resources? Message The Mods to find out how! You would rather build a relationship with the <a href="/r/CyberSecurity">/r/CyberSecurity</a> community than get banned! Please message the mods before posting links to your own projects or if you have any questions about the advertising policies</p></li> +<li><p><strong>No Personally-Identifiable Information</strong> +Do not post personally-identifiable information, unless the source has consented to it.</p></li> +</ol> + +<hr/> + +<p><strong>This subreddit is oriented towards computer security professionals</strong></p> + +<p>Need help with a computer security problem? </p> + +<ul> +<li><p><a href="/r/cybersecurity_help">/r/cybersecurity_help</a></p></li> +<li><p><a href="/r/cybersecurity101">/r/cybersecurity101</a></p></li> +</ul> + +<p>Are you looking for a job or looking to hire someone?</p> + +<ul> +<li><a href="/r/cybersecurityjobs">/r/cybersecurityjobs</a> (currently closed)</li> +</ul> + +<p>Are you looking for home defense and security systems (alarms, CCTV, ect)? </p> + +<ul> +<li><a href="/r/Locksmith">/r/Locksmith</a></li> +<li><a href="/r/homedefense">/r/homedefense</a></li> +<li><a href="/r/homeautomation">/r/homeautomation</a></li> +<li><a href="/r/videosurveillance">/r/videosurveillance</a></li> +<li><a href="/r/physec/">/r/physec/</a></li> +</ul> + +<p>Are you a security guard or physical security professional? </p> + +<ul> +<li><a href="/r/ProtectAndServe">/r/ProtectAndServe</a></li> +<li><a href="/r/Secguards">/r/Secguards</a> </li> +</ul> + +<p>Are you here to post an advertisement or spam? </p> + +<ul> +<li><a href="/r/spam">/r/spam</a></li> +</ul> + +<p>Other Security Communities:</p> + +<ul> +<li><a href="/r/ComputerSecurity">/r/ComputerSecurity</a></li> +<li><a href="/r/crypto">/r/crypto</a> </li> +<li><a href="/r/hacking">/r/hacking</a> </li> +<li><a href="/r/netdef">/r/netdef</a><br/></li> +<li><a href="/r/intelligence">/r/intelligence</a></li> +<li><a href="/r/opsec">/r/opsec</a></li> +<li><a href="/r/OperationsSecurity">/r/OperationsSecurity</a></li> +<li><a href="/r/privacy">/r/privacy</a> </li> +<li><a href="/r/redteam">/r/redteam</a></li> +</ul> + +<p>Just for fun:</p> + +<ul> +<li><a href="/r/scambaiting">/r/scambaiting</a></li> +</ul> + +<hr/> + +<hr/> + +<p>We ask all users with a potential conflict of interest (e.g. security product manufacturers and service providers) to disclose their affiliation. This allows subscribers to ask them questions about their areas of expertise while ensuring transparency. For questions about this status, to request a user flair, or if you think that these users have violated this subreddit's policies, please <a href="http://www.reddit.com/message/compose?to=%2Fr%2Fcybersecurity">message the mods</a>.</p> +</div> +</div></form><div class="bottom"><span class="age">a community for <time title="Tue May 22 05:07:59 2012 UTC" datetime="2012-05-22T05:07:59+00:00">11 years</time></span></div></div></div><div class='spacer'></div><div class='spacer'><div class="sidecontentbox " ><div class="title"><h1>MODERATORS</h1></div><ul class="content"><li class="message-button centered"><a class="c-btn c-btn-primary login-required" href="/message/compose/?to=/r/cybersecurity">message the mods</a></li></ul></div></div><div class='spacer'><div class="read-next-container"><aside class="read-next"><header class="read-next-header"><div class="read-next-header-title">discussions in <a href="https://www.reddit.com/r/cybersecurity/?ref=readnext" >r/cybersecurity</a></div><nav class="read-next-nav read-next-nav-left"><span class="read-next-button prev"><</span><span class="read-next-button next">></span></nav><div class="read-next-nav read-next-nav-right"><span class="read-next-dismiss">X</span></div></header><div class="read-next-list"><div class="listing read-next-listing"><div class="listing-contents"><a id="read-next-link-t3_1au4n18" class="read-next-link may-blank" href="/r/cybersecurity/comments/1au4n18/what_are_some_musthave_rules_and_policies_that/?ref=readnext"><div class="read-next-meta"><span class="score" title="106">106</span>  · <span class="comments">45 comments</span> </div><div class="read-next-title">What are some "must-have" rules and policies that you configure on every firewall you worked with?</div></a><a id="read-next-link-t3_1au7265" class="read-next-link may-blank" href="/r/cybersecurity/comments/1au7265/has_it_been_anyone_elses_experience_when_meeting/?ref=readnext"><div class="read-next-meta"><span class="score" title="55">55</span>  · <span class="comments">27 comments</span> </div><div class="read-next-title">Has it been anyone else's experience when meeting people in the field and networking that they all got their positions because they knew someone even with zero experience?</div></a><a id="read-next-link-t3_1aty4yv" class="read-next-link may-blank" href="/r/cybersecurity/comments/1aty4yv/when_it_comes_to_cyber_threats_india_not_the_us/?ref=readnext"><div class="read-next-meta"><span class="score" title="116">116</span>  · <span class="comments">17 comments</span> </div><div class="read-next-thumbnail"><img src="//b.thumbs.redditmedia.com/fN4jxU3yZomnnLik7d9diWdRnTkbl67V4YjF0oXfSfY.jpg" width='70' height='36' alt=""></div><div class="read-next-title">When it comes to cyber threats, India – not the US – is China’s biggest concern</div></a><a id="read-next-link-t3_1atmron" class="read-next-link may-blank" href="/r/cybersecurity/comments/1atmron/gpt4_can_hack_websites_with_733_success_rate_in/?ref=readnext"><div class="read-next-meta"><span class="score" title="449">449</span>  · <span class="comments">57 comments</span> </div><div class="read-next-thumbnail"><img src="//a.thumbs.redditmedia.com/grGQ4Xa2kcPcM6pqkNIDlUWrOa_1Q98cv7xt9EOyyg8.jpg" width='70' height='36' alt=""></div><div class="read-next-title">GPT4 can hack websites with 73.3% success rate in sandboxed environment</div></a><a id="read-next-link-t3_1aubpd0" class="read-next-link may-blank" href="/r/cybersecurity/comments/1aubpd0/bouncing_back_from_a_cyber_attack/?ref=readnext"><div class="read-next-meta"></div><div class="read-next-thumbnail"><img src="//b.thumbs.redditmedia.com/Jefbctam7l-lzDPxyKfR_L5oOV68nxzrg7E89LrTfpQ.jpg" width='70' height='23' alt=""></div><div class="read-next-title">Bouncing back from a cyber attack</div></a><a id="read-next-link-t3_1au92zu" class="read-next-link may-blank" href="/r/cybersecurity/comments/1au92zu/mentorship_monday_post_all_career_education_and/?ref=readnext"><div class="read-next-meta"><span class="score" title="5">5</span>  · <span class="comments">8 comments</span> </div><div class="read-next-title">Mentorship Monday - Post All Career, Education and Job questions here!</div></a><a id="read-next-link-t3_1atjev1" class="read-next-link may-blank" href="/r/cybersecurity/comments/1atjev1/what_is_a_security_feature_that_is_really/?ref=readnext"><div class="read-next-meta"><span class="score" title="192">192</span>  · <span class="comments">296 comments</span> </div><div class="read-next-title">What is a security feature that is really "security theater"?</div></a><a id="read-next-link-t3_1atltdb" class="read-next-link may-blank" href="/r/cybersecurity/comments/1atltdb/resources_to_learn_threat_hunting/?ref=readnext"><div class="read-next-meta"><span class="score" title="94">94</span>  · <span class="comments">17 comments</span> </div><div class="read-next-title">Resources to learn Threat Hunting?</div></a><a id="read-next-link-t3_1atuoz8" class="read-next-link may-blank" href="/r/cybersecurity/comments/1atuoz8/seeking_cybersecurity_awareness_materials_for/?ref=readnext"><div class="read-next-meta"><span class="score" title="15">15</span>  · <span class="comments">9 comments</span> </div><div class="read-next-title">Seeking Cybersecurity Awareness Materials for School Teenagers age group students: Any Recommendations?</div></a><a id="read-next-link-t3_1au1z7n" class="read-next-link may-blank" href="/r/cybersecurity/comments/1au1z7n/fbis_mostwanted_zeus_and_icedid_malware/?ref=readnext"><div class="read-next-meta"><span class="score" title="4">4</span>  · <span class="comments">1 comment</span> </div><div class="read-next-thumbnail"><img src="//b.thumbs.redditmedia.com/sKOGKVqS9KxTIcMVdCAsY2em1FWOai0QXCxUcxjoW7U.jpg" width='70' height='36' alt=""></div><div class="read-next-title">FBI's Most-Wanted Zeus and IcedID Malware Mastermind Pleads Guilty</div></a></div></div></div></aside></div></div></div><a name="content"></a><div class="content" role="main" ><section class="infobar listingsignupbar"><a href="/login" class="login-required listingsignupbar__container"><h2 class="listingsignupbar__title">Welcome to Reddit,</h2><p class="listingsignupbar__desc">the front page of the internet.</p><div class="listingsignupbar__cta-container"><span class="c-btn c-btn-primary c-pull-left listingsignupbar__cta-button">Become a Redditor</span><p class="listingsignupbar__cta-desc">and join one of thousands of communities.</p></div></a><a href="#" class="listingsignupbar__close" title="close">×</a></section><style>body >.content .link .rank, .rank-spacer { width: 1.1ex } body >.content .link .midcol, .midcol-spacer { width: 4.1ex } .adsense-wrap { background-color: #eff7ff; font-size: 18px; padding-left: 5.2ex; padding-right: 5px; }</style><div id="siteTable" class="sitetable linklisting"><div class=" thing id-t3_16nf2ev linkflair odd  link self" id="thing_t3_16nf2ev" onclick="click_thing(this)" data-fullname="t3_16nf2ev" data-type="link" data-gildings="0" data-whitelist-status="all_ads" data-is-gallery="false" data-author="skeedooshski" data-author-fullname="t2_2w3lp8yl" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-timestamp="1695195276000" data-url="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-domain="self.cybersecurity" data-rank="" data-comments-count="98" data-score="241" data-promoted="false" data-nsfw="false" data-spoiler="false" data-oc="false" data-num-crossposts="0" data-context="comments" ><p class="parent"></p><span class="rank"></span><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="score dislikes" title="240">240</div><div class="score unvoted" title="241">241</div><div class="score likes" title="242">242</div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><a class="thumbnail invisible-when-pinned self may-blank " data-event-action="thumbnail" href="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" ></a><div class="entry unvoted"><div class="top-matter"><p class="title"><a class="title may-blank " data-event-action="title" href="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" tabindex="1" >Twitter/X is getting weirder; where now for security news and analysis?</a><span class="flairrichtext flaircolordark linkflairlabel " title="News - General" style="background-color: #0079d3; border-color: #0079d3;"><span>News - General</span></span> <span class="domain">(<a href="/r/cybersecurity/">self.cybersecurity</a>)</span></p><p class="tagline ">submitted <time title="Wed Sep 20 07:34:36 2023 UTC" datetime="2023-09-20T07:34:36+00:00" class="live-timestamp">5 months ago</time> by <a href="https://www.reddit.com/user/skeedooshski" class="author may-blank id-t2_2w3lp8yl" >skeedooshski</a><span class="userattrs"></span></p></div><div class="expando expando-uninitialized" data-pin-condition="function() {return this.style.display != 'none';}" ><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t3_16nf2evdgw"><input type="hidden" name="thing_id" value="t3_16nf2ev"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I primarily use Twitter/X as a glorified RSS feed of security news and analysis reporting. Lately its getting heavier on ads and weird pushed content. </p> + +<p>I'm not for replacing it with 5 different platforms, but if I was going to use something else, what are peeps using?</p> +</div> +</div></form></div><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-event-action="comments" class="bylink comments may-blank" rel="nofollow" >98 comments</a></li><li class="share"><a class="post-sharing-button" href="javascript: void 0;">share</a></li><li class="link-save-button save-button login-required"><a href="#">save</a></li><li><form action="/post/hide" method="post" class="state-button hide-button"><input type="hidden" name="executed" value="hidden" /><span><a href="javascript:void(0)" class=" " data-event-action="hide" onclick="change_state(this, 'hide', hide_thing);">hide</a></span></form></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li></ul><div class="reportform report-t3_16nf2ev"></div></div><div class="child" ></div><div class="clearleft"></div></div><div class="clearleft"></div></div><div class='commentarea'><div class="panestack-title"><span class="title">all 98 comments</span></div><div class="menuarea"><div class="spacer"><span class="dropdown-title lightdrop">sorted by: </span><div class="dropdown lightdrop" onclick="open_menu(this)"><span class="selected">best</span></div><div class="drop-choices lightdrop"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/?sort=top" class="choice" >top</a><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/?sort=new" class="choice" >new</a><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/?sort=controversial" class="choice" >controversial</a><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/?sort=old" class="choice" >old</a><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/?sort=random" class="hidden choice" >random</a><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/?sort=qa" class="choice" >q&a</a><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/?sort=live" class="hidden choice" >live (beta)</a></div></div><div class="spacer"></div></div><section class="infobar commentsignupbar"><div class="commentsignupbar__container"><a href="/login" class="login-required commentsignupbar__link-wrapper"><textarea class="commentsignupbar__textarea"></textarea><div class="commentsignupbar__textarea-above"><h2 class="commentsignupbar__title">Want to add to the discussion?</h2><p class="commentsignupbar__desc">Post a comment!</p><div class="commentsignupbar__cta-container"><span class="c-btn c-btn-primary commentsignupbar__cta-button">Create an account</span></div></div></a></div></section><div id="siteTable_t3_16nf2ev" class="sitetable nestedlisting"><div class=" thing id-t1_k1f2su1 noncollapsed   comment " id="thing_t1_k1f2su1" onclick="click_thing(this)" data-fullname="t1_k1f2su1" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="goretsky" data-author-fullname="t2_3tihk" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f2su1/" ><p class="parent"><a name="k1f2su1"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/goretsky" class="author may-blank id-t2_3tihk" >goretsky</a><span class="flairrichtext flaircolordark flair " title="Aryeh Goretsky" style="background-color: #edeff1; border-color: #edeff1;"><span>Aryeh Goretsky</span></span><span class="userattrs"></span> <span class="score dislikes" title="163">163 points</span><span class="score unvoted" title="164">164 points</span><span class="score likes" title="165">165 points</span> <time title="Wed Sep 20 14:11:13 2023 UTC" datetime="2023-09-20T14:11:13+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(5 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f2su1eco"><input type="hidden" name="thing_id" value="t1_k1f2su1"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Hello,</p> + +<p>I use Reddit for this purpose. I have created several security themed multireddits for the purpose of tracking security-related topics:</p> + +<p><a href="https://old.reddit.com/user/goretsky/m/security/">https://old.reddit.com/user/goretsky/m/security/</a> - tracks about 90 active security-related subreddits, but no vendors or open source projects and is regularly pruned of inactive subreddits.</p> + +<p><a href="https://old.reddit.com/user/goretsky/m/security_vendor/">https://old.reddit.com/user/goretsky/m/security_vendor/</a> - tracks about three dozen security vendor and open source project subreddits. </p> + +<p><a href="https://old.reddit.com/user/goretsky/m/security_inactive/">https://old.reddit.com/user/goretsky/m/security_inactive/</a> - a multireddit specifically for subreddits that were in the first two, but no longer seem to be active (lets me periodically check them for activity) </p> + +<p>You can view these by new, hot, top and so forth to get ideas of what's current, might be an emerging issue, what was historically significant, and so forth.</p> + +<p>Regards,</p> + +<p>Aryeh Goretsky</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f2su1/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f2su1/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f2su1"></div></div><div class="child"><div id="siteTable_t1_k1f2su1" class="sitetable listing"><div class=" thing id-t1_k1gtm9x noncollapsed   comment " id="thing_t1_k1gtm9x" onclick="click_thing(this)" data-fullname="t1_k1gtm9x" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="cccanterbury" data-author-fullname="t2_93f0t46r4" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gtm9x/" ><p class="parent"><a name="k1gtm9x"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/cccanterbury" class="author may-blank id-t2_93f0t46r4" >cccanterbury</a><span class="userattrs"></span> <span class="score dislikes" title="7">7 points</span><span class="score unvoted" title="8">8 points</span><span class="score likes" title="9">9 points</span> <time title="Wed Sep 20 20:17:18 2023 UTC" datetime="2023-09-20T20:17:18+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1gtm9x948"><input type="hidden" name="thing_id" value="t1_k1gtm9x"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Wow thanks Aryeh!</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gtm9x/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gtm9x/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f2su1" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1gtm9x"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1hmfej noncollapsed   comment " id="thing_t1_k1hmfej" onclick="click_thing(this)" data-fullname="t1_k1hmfej" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="kitwillybb" data-author-fullname="t2_fdvexzhhd" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hmfej/" ><p class="parent"><a name="k1hmfej"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/kitwillybb" class="author may-blank id-t2_fdvexzhhd" >kitwillybb</a><span class="userattrs"></span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Wed Sep 20 23:15:13 2023 UTC" datetime="2023-09-20T23:15:13+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1hmfejqzw"><input type="hidden" name="thing_id" value="t1_k1hmfej"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Thanks for this.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hmfej/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hmfej/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f2su1" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1hmfej"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1i7bji noncollapsed   comment " id="thing_t1_k1i7bji" onclick="click_thing(this)" data-fullname="t1_k1i7bji" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Robbbbbbbbb" data-author-fullname="t2_a6lsi" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1i7bji/" ><p class="parent"><a name="k1i7bji"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Robbbbbbbbb" class="author may-blank id-t2_a6lsi" >Robbbbbbbbb</a><span class="userattrs"></span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Thu Sep 21 01:39:52 2023 UTC" datetime="2023-09-21T01:39:52+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1i7bjic0u"><input type="hidden" name="thing_id" value="t1_k1i7bji"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Thanks! Great multireddits</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1i7bji/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1i7bji/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f2su1" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1i7bji"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1mafik noncollapsed   comment " id="thing_t1_k1mafik" onclick="click_thing(this)" data-fullname="t1_k1mafik" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="That_CatDad" data-author-fullname="t2_4d8ihz1f" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1mafik/" ><p class="parent"><a name="k1mafik"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/That_CatDad" class="author may-blank id-t2_4d8ihz1f" >That_CatDad</a><span class="userattrs"></span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Thu Sep 21 20:49:59 2023 UTC" datetime="2023-09-21T20:49:59+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1mafikfpn"><input type="hidden" name="thing_id" value="t1_k1mafik"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Well I just discovered multireddits, thank you so much this will definitely change how I use this site</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1mafik/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1mafik/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f2su1" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1mafik"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing noncollapsed   deleted comment " onclick="click_thing(this)" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ehfna/" ><p class="parent"></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><em>[deleted]</em> <time title="Wed Sep 20 11:30:36 2023 UTC" datetime="2023-09-20T11:30:36+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(6 children)</a></p><div class="usertext grayed"><input type="hidden" name="thing_id" value=""/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>[deleted]</p> +</div> +</div></div><ul class="flat-list buttons"></ul><div class="reportform report-t1_k1ehfna"></div></div><div class="child"><div id="siteTable_deleted" class="sitetable listing"><div class=" thing id-t1_k1gmnbd noncollapsed   comment " id="thing_t1_k1gmnbd" onclick="click_thing(this)" data-fullname="t1_k1gmnbd" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="uncannysalt" data-author-fullname="t2_3wpcoju1" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gmnbd/" ><p class="parent"><a name="k1gmnbd"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/uncannysalt" class="author may-blank id-t2_3wpcoju1" >uncannysalt</a><span class="flairrichtext flaircolorlight flair " title="Security Architect" style="background-color: #373c3f; border-color: #373c3f;"><span>Security Architect</span></span><span class="userattrs"></span> <span class="score dislikes" title="12">12 points</span><span class="score unvoted" title="13">13 points</span><span class="score likes" title="14">14 points</span> <time title="Wed Sep 20 19:38:55 2023 UTC" datetime="2023-09-20T19:38:55+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1gmnbdxe8"><input type="hidden" name="thing_id" value="t1_k1gmnbd"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Exactly. Most prolific security folks have feeds available.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gmnbd/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gmnbd/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1gmnbd"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1gobsy noncollapsed   comment " id="thing_t1_k1gobsy" onclick="click_thing(this)" data-fullname="t1_k1gobsy" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="th4ntis" data-author-fullname="t2_5i6unrry" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gobsy/" ><p class="parent"><a name="k1gobsy"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/th4ntis" class="author may-blank id-t2_5i6unrry" >th4ntis</a><span class="userattrs"></span> <span class="score dislikes" title="5">5 points</span><span class="score unvoted" title="6">6 points</span><span class="score likes" title="7">7 points</span> <time title="Wed Sep 20 19:48:06 2023 UTC" datetime="2023-09-20T19:48:06+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(4 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1gobsytgn"><input type="hidden" name="thing_id" value="t1_k1gobsy"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I would actually love to do this but need to figure out how. I haven't looked into it yet but this is on my list of things to do.<br/> +Any recommendations or tips would be helpful.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gobsy/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gobsy/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1gobsy"></div></div><div class="child"><div id="siteTable_t1_k1gobsy" class="sitetable listing"><div class=" thing noncollapsed   deleted comment " onclick="click_thing(this)" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1grrni/" ><p class="parent"></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><em>[deleted]</em> <time title="Wed Sep 20 20:07:09 2023 UTC" datetime="2023-09-20T20:07:09+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(2 children)</a></p><div class="usertext grayed"><input type="hidden" name="thing_id" value=""/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>[deleted]</p> +</div> +</div></div><ul class="flat-list buttons"></ul><div class="reportform report-t1_k1grrni"></div></div><div class="child"><div id="siteTable_deleted" class="sitetable listing"><div class=" thing id-t1_k1mbol3 noncollapsed   comment " id="thing_t1_k1mbol3" onclick="click_thing(this)" data-fullname="t1_k1mbol3" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="th4ntis" data-author-fullname="t2_5i6unrry" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1mbol3/" ><p class="parent"><a name="k1mbol3"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/th4ntis" class="author may-blank id-t2_5i6unrry" >th4ntis</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Thu Sep 21 20:57:08 2023 UTC" datetime="2023-09-21T20:57:08+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1mbol3bgn"><input type="hidden" name="thing_id" value="t1_k1mbol3"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>For someone getting started, any RSS links you recommend?</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1mbol3/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1mbol3/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1mbol3"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1k9di0 noncollapsed   comment " id="thing_t1_k1k9di0" onclick="click_thing(this)" data-fullname="t1_k1k9di0" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Bleord" data-author-fullname="t2_6qov61wx" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1k9di0/" ><p class="parent"><a name="k1k9di0"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Bleord" class="author may-blank id-t2_6qov61wx" >Bleord</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Thu Sep 21 13:40:03 2023 UTC" datetime="2023-09-21T13:40:03+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1k9di0ftw"><input type="hidden" name="thing_id" value="t1_k1k9di0"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Lots of web browsers have it built in.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1k9di0/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1k9di0/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1gobsy" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1k9di0"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1eutsw noncollapsed   comment " id="thing_t1_k1eutsw" onclick="click_thing(this)" data-fullname="t1_k1eutsw" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="beagle_bathouse" data-author-fullname="t2_83rlaeff" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eutsw/" ><p class="parent"><a name="k1eutsw"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/beagle_bathouse" class="author may-blank id-t2_83rlaeff" >beagle_bathouse</a><span class="userattrs"></span> <span class="score dislikes" title="59">59 points</span><span class="score unvoted" title="60">60 points</span><span class="score likes" title="61">61 points</span> <time title="Wed Sep 20 13:17:09 2023 UTC" datetime="2023-09-20T13:17:09+00:00" class="live-timestamp">5 months ago</time><time class="edited-timestamp" title="last edited 9 days ago" datetime="2024-02-09T18:57:48+00:00">*</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(8 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1eutswfzs"><input type="hidden" name="thing_id" value="t1_k1eutsw"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>desert person pet quiet worm hurry absorbed like cause rainstorm</p> + +<p><em>This post was mass deleted and anonymized with <a href="https://redact.dev">Redact</a></em></p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eutsw/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eutsw/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1eutsw"></div></div><div class="child"><div id="siteTable_t1_k1eutsw" class="sitetable listing"><div class=" thing id-t1_k1fe1am noncollapsed   comment " id="thing_t1_k1fe1am" onclick="click_thing(this)" data-fullname="t1_k1fe1am" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="volume_two" data-author-fullname="t2_dmeg9cabu" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fe1am/" ><p class="parent"><a name="k1fe1am"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/volume_two" class="author may-blank id-t2_dmeg9cabu" >volume_two</a><span class="userattrs"></span> <span class="score dislikes" title="12">12 points</span><span class="score unvoted" title="13">13 points</span><span class="score likes" title="14">14 points</span> <time title="Wed Sep 20 15:20:53 2023 UTC" datetime="2023-09-20T15:20:53+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(2 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fe1am0rj"><input type="hidden" name="thing_id" value="t1_k1fe1am"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Nazis have free speech rights too! And the Constitution guarantees their freedom of speech on X.</p> + +<p>You non-Nazis are the nazis!</p> + +<h1>/S</h1> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fe1am/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fe1am/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1eutsw" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fe1am"></div></div><div class="child"><div id="siteTable_t1_k1fe1am" class="sitetable listing"><div class=" thing id-t1_k1ffalw noncollapsed   comment " id="thing_t1_k1ffalw" onclick="click_thing(this)" data-fullname="t1_k1ffalw" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="beagle_bathouse" data-author-fullname="t2_83rlaeff" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ffalw/" ><p class="parent"><a name="k1ffalw"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/beagle_bathouse" class="author may-blank id-t2_83rlaeff" >beagle_bathouse</a><span class="userattrs"></span> <span class="score dislikes" title="9">9 points</span><span class="score unvoted" title="10">10 points</span><span class="score likes" title="11">11 points</span> <time title="Wed Sep 20 15:28:26 2023 UTC" datetime="2023-09-20T15:28:26+00:00" class="live-timestamp">5 months ago</time><time class="edited-timestamp" title="last edited 9 days ago" datetime="2024-02-09T18:57:43+00:00">*</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1ffalwqyw"><input type="hidden" name="thing_id" value="t1_k1ffalw"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>wakeful mighty steer fragile quickest rinse weather saw melodic boast</p> + +<p><em>This post was mass deleted and anonymized with <a href="https://redact.dev">Redact</a></em></p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ffalw/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ffalw/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1fe1am" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1ffalw"></div></div><div class="child"><div id="siteTable_t1_k1ffalw" class="sitetable listing"><div class=" thing id-t1_k1fgnqb noncollapsed   comment " id="thing_t1_k1fgnqb" onclick="click_thing(this)" data-fullname="t1_k1fgnqb" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="volume_two" data-author-fullname="t2_dmeg9cabu" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fgnqb/" ><p class="parent"><a name="k1fgnqb"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/volume_two" class="author may-blank id-t2_dmeg9cabu" >volume_two</a><span class="userattrs"></span> <span class="score dislikes" title="-1">-1 points</span><span class="score unvoted" title="0">0 points</span><span class="score likes" title="1">1 point</span> <time title="Wed Sep 20 15:36:36 2023 UTC" datetime="2023-09-20T15:36:36+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fgnqbj60"><input type="hidden" name="thing_id" value="t1_k1fgnqb"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Somebody give this person an upvote!</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fgnqb/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fgnqb/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1ffalw" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fgnqb"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1fx6xl collapsed collapsed-for-reason   comment " id="thing_t1_k1fx6xl" onclick="click_thing(this)" data-fullname="t1_k1fx6xl" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="stacksmasher" data-author-fullname="t2_4ceji" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fx6xl/" ><p class="parent"><a name="k1fx6xl"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[+]</a><a href="https://www.reddit.com/user/stacksmasher" class="author may-blank id-t2_4ceji" >stacksmasher</a><span class="userattrs"></span> <span class="collapsed-reason">comment score below threshold</span><span class="score dislikes" title="-8">-8 points</span><span class="score unvoted" title="-7">-7 points</span><span class="score likes" title="-6">-6 points</span> <time title="Wed Sep 20 17:13:37 2023 UTC" datetime="2023-09-20T17:13:37+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(4 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fx6xluja"><input type="hidden" name="thing_id" value="t1_k1fx6xl"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Infosec.exchange is horrible with very little engagement</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fx6xl/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fx6xl/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1eutsw" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fx6xl"></div></div><div class="child"><div id="siteTable_t1_k1fx6xl" class="sitetable listing"><div class=" thing id-t1_k1hdzxo noncollapsed   comment " id="thing_t1_k1hdzxo" onclick="click_thing(this)" data-fullname="t1_k1hdzxo" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="thomasareed" data-author-fullname="t2_ogy3b" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hdzxo/" ><p class="parent"><a name="k1hdzxo"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/thomasareed" class="author may-blank id-t2_ogy3b" >thomasareed</a><span class="userattrs"></span> <span class="score dislikes" title="5">5 points</span><span class="score unvoted" title="6">6 points</span><span class="score likes" title="7">7 points</span> <time title="Wed Sep 20 22:18:19 2023 UTC" datetime="2023-09-20T22:18:19+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1hdzxo59d"><input type="hidden" name="thing_id" value="t1_k1hdzxo"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Hard disagree. What you get out of it depends on what you put in. If you’re just looking to curate a feed of infosec news, this may not be it. If you’re looking for a group of friends to have interesting discussions with, both infosec-related and not, you can make it that place. But that happens slowly and with participation.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hdzxo/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hdzxo/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1fx6xl" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1hdzxo"></div></div><div class="child"><div id="siteTable_t1_k1hdzxo" class="sitetable listing"><div class=" thing id-t1_k1hh7ow noncollapsed   comment " id="thing_t1_k1hh7ow" onclick="click_thing(this)" data-fullname="t1_k1hh7ow" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="stacksmasher" data-author-fullname="t2_4ceji" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hh7ow/" ><p class="parent"><a name="k1hh7ow"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/stacksmasher" class="author may-blank id-t2_4ceji" >stacksmasher</a><span class="userattrs"></span> <span class="score dislikes" title="-3">-3 points</span><span class="score unvoted" title="-2">-2 points</span><span class="score likes" title="-1">-1 points</span> <time title="Wed Sep 20 22:39:33 2023 UTC" datetime="2023-09-20T22:39:33+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1hh7owe2s"><input type="hidden" name="thing_id" value="t1_k1hh7ow"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Nope I only use it for data. You know most critical issues are announced on Twitter?</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hh7ow/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hh7ow/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1hdzxo" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1hh7ow"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1fyt2y noncollapsed   comment " id="thing_t1_k1fyt2y" onclick="click_thing(this)" data-fullname="t1_k1fyt2y" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="beagle_bathouse" data-author-fullname="t2_83rlaeff" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fyt2y/" ><p class="parent"><a name="k1fyt2y"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/beagle_bathouse" class="author may-blank id-t2_83rlaeff" >beagle_bathouse</a><span class="userattrs"></span> <span class="score dislikes" title="5">5 points</span><span class="score unvoted" title="6">6 points</span><span class="score likes" title="7">7 points</span> <time title="Wed Sep 20 17:22:55 2023 UTC" datetime="2023-09-20T17:22:55+00:00" class="live-timestamp">5 months ago</time><time class="edited-timestamp" title="last edited 9 days ago" datetime="2024-02-09T18:57:28+00:00">*</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fyt2yioo"><input type="hidden" name="thing_id" value="t1_k1fyt2y"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>erect narrow wipe plant tub nail rain shame angle unpack</p> + +<p><em>This post was mass deleted and anonymized with <a href="https://redact.dev">Redact</a></em></p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fyt2y/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fyt2y/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1fx6xl" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fyt2y"></div></div><div class="child"><div id="siteTable_t1_k1fyt2y" class="sitetable listing"><div class=" thing id-t1_k1gafku noncollapsed   comment " id="thing_t1_k1gafku" onclick="click_thing(this)" data-fullname="t1_k1gafku" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="stacksmasher" data-author-fullname="t2_4ceji" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gafku/" ><p class="parent"><a name="k1gafku"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/stacksmasher" class="author may-blank id-t2_4ceji" >stacksmasher</a><span class="userattrs"></span> <span class="score dislikes" title="-3">-3 points</span><span class="score unvoted" title="-2">-2 points</span><span class="score likes" title="-1">-1 points</span> <time title="Wed Sep 20 18:29:55 2023 UTC" datetime="2023-09-20T18:29:55+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1gafkuw1o"><input type="hidden" name="thing_id" value="t1_k1gafku"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Agree 1000000000% but the exchange platform is not it</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gafku/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gafku/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1fyt2y" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1gafku"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1ed9ru noncollapsed   comment " id="thing_t1_k1ed9ru" onclick="click_thing(this)" data-fullname="t1_k1ed9ru" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="sonicoak" data-author-fullname="t2_40lq8qtkt" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ed9ru/" ><p class="parent"><a name="k1ed9ru"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/sonicoak" class="author may-blank id-t2_40lq8qtkt" >sonicoak</a><span class="flairrichtext flaircolorlight flair " title="Governance, Risk, & Compliance" style="background-color: #373c3f; border-color: #373c3f;"><span>Governance, Risk, & Compliance</span></span><span class="userattrs"></span> <span class="score dislikes" title="96">96 points</span><span class="score unvoted" title="97">97 points</span><span class="score likes" title="98">98 points</span> <time title="Wed Sep 20 10:49:11 2023 UTC" datetime="2023-09-20T10:49:11+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(19 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1ed9rulb2"><input type="hidden" name="thing_id" value="t1_k1ed9ru"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p><a href="https://infosec.exchange/home">https://infosec.exchange/home</a> , it is a Mastodon server.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ed9ru/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ed9ru/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1ed9ru"></div></div><div class="child"><div id="siteTable_t1_k1ed9ru" class="sitetable listing"><div class=" thing id-t1_k1f2191 noncollapsed   comment " id="thing_t1_k1f2191" onclick="click_thing(this)" data-fullname="t1_k1f2191" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Chrishamilton2007" data-author-fullname="t2_5goj3" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f2191/" ><p class="parent"><a name="k1f2191"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Chrishamilton2007" class="author may-blank id-t2_5goj3" >Chrishamilton2007</a><span class="userattrs"></span> <span class="score dislikes" title="80">80 points</span><span class="score unvoted" title="81">81 points</span><span class="score likes" title="82">82 points</span> <time title="Wed Sep 20 14:06:15 2023 UTC" datetime="2023-09-20T14:06:15+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(4 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f21918rv"><input type="hidden" name="thing_id" value="t1_k1f2191"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I dunno, I'm sure Jerry Bell is cool and all but I'm hesitant to point people to what could essentially turn into a private facebook group overnight.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f2191/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f2191/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1ed9ru" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f2191"></div></div><div class="child"><div id="siteTable_t1_k1f2191" class="sitetable listing"><div class=" thing id-t1_k1fckr4 noncollapsed   comment " id="thing_t1_k1fckr4" onclick="click_thing(this)" data-fullname="t1_k1fckr4" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Versed_Percepton" data-author-fullname="t2_94cm2jqqy" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fckr4/" ><p class="parent"><a name="k1fckr4"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Versed_Percepton" class="author may-blank id-t2_94cm2jqqy" >Versed_Percepton</a><span class="userattrs"></span> <span class="score dislikes" title="20">20 points</span><span class="score unvoted" title="21">21 points</span><span class="score likes" title="22">22 points</span> <time title="Wed Sep 20 15:12:00 2023 UTC" datetime="2023-09-20T15:12:00+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fckr4d58"><input type="hidden" name="thing_id" value="t1_k1fckr4"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Jerry has <a href="https://Infosec.exchange">Infosec.exchange</a> open and very rarely are there admin issues at the federation level. Also, I have NEVER seen Jerry pull a 'shitty admin move' in the couple years I have been on the instance. Let me tell you, there were times I wish he would. But he is just not that type of person.</p> + +<p>He has been working his ass off to expand the instance to support the influx of new users, he is very open about this in his daily feeds too. So, there is no way in hell the instance will become 'private' with all the hard work he has put in here.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fckr4/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fckr4/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f2191" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fckr4"></div></div><div class="child"><div id="siteTable_t1_k1fckr4" class="sitetable listing"><div class=" thing id-t1_k1flqhj noncollapsed   comment " id="thing_t1_k1flqhj" onclick="click_thing(this)" data-fullname="t1_k1flqhj" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="moker" data-author-fullname="t2_31fvu" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1flqhj/" ><p class="parent"><a name="k1flqhj"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/moker" class="author may-blank id-t2_31fvu" >moker</a><span class="userattrs"></span> <span class="score dislikes" title="4">4 points</span><span class="score unvoted" title="5">5 points</span><span class="score likes" title="6">6 points</span> <time title="Wed Sep 20 16:06:31 2023 UTC" datetime="2023-09-20T16:06:31+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1flqhjact"><input type="hidden" name="thing_id" value="t1_k1flqhj"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>thank you :)</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1flqhj/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1flqhj/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1fckr4" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1flqhj"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1f3k7m noncollapsed   comment " id="thing_t1_k1f3k7m" onclick="click_thing(this)" data-fullname="t1_k1f3k7m" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Gangrif" data-author-fullname="t2_13q213" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f3k7m/" ><p class="parent"><a name="k1f3k7m"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Gangrif" class="author may-blank id-t2_13q213" >Gangrif</a><span class="userattrs"></span> <span class="score dislikes" title="16">16 points</span><span class="score unvoted" title="17">17 points</span><span class="score likes" title="18">18 points</span> <time title="Wed Sep 20 14:16:09 2023 UTC" datetime="2023-09-20T14:16:09+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f3k7m4bq"><input type="hidden" name="thing_id" value="t1_k1f3k7m"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>for what it’s worth. jerry has been tirelessly administering infosec.exchange for years. and doesn’t seem likely to stop. and if he does…. you can move easily to another instance. defcon runs one, the mastodon folks run one. i run one (though mine is mainly for me.) and they all federate with the others. so you don’t miss out being on you’re own or one other than your friends are on.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f3k7m/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f3k7m/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f2191" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f3k7m"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1h3ys0 noncollapsed   comment " id="thing_t1_k1h3ys0" onclick="click_thing(this)" data-fullname="t1_k1h3ys0" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="moker" data-author-fullname="t2_31fvu" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h3ys0/" ><p class="parent"><a name="k1h3ys0"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/moker" class="author may-blank id-t2_31fvu" >moker</a><span class="userattrs"></span> <span class="score dislikes" title="7">7 points</span><span class="score unvoted" title="8">8 points</span><span class="score likes" title="9">9 points</span> <time title="Wed Sep 20 21:17:06 2023 UTC" datetime="2023-09-20T21:17:06+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1h3ys0lx7"><input type="hidden" name="thing_id" value="t1_k1h3ys0"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>yes. After 7 years of being a free and open forum, I have decided to make <a href="https://infosec.exchange">infosec.exchange</a> a private facebook group. I had been on the fence about it until now.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h3ys0/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h3ys0/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f2191" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1h3ys0"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1expm6 noncollapsed   comment " id="thing_t1_k1expm6" onclick="click_thing(this)" data-fullname="t1_k1expm6" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="movement2012" data-author-fullname="t2_tuqrm" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1expm6/" ><p class="parent"><a name="k1expm6"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/movement2012" class="author may-blank id-t2_tuqrm" >movement2012</a><span class="userattrs"></span> <span class="score dislikes" title="17">17 points</span><span class="score unvoted" title="18">18 points</span><span class="score likes" title="19">19 points</span> <time title="Wed Sep 20 13:37:17 2023 UTC" datetime="2023-09-20T13:37:17+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(6 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1expm616a"><input type="hidden" name="thing_id" value="t1_k1expm6"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Mastodon feels a bit dry. Are there too few people, or am I not following enough?</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1expm6/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1expm6/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1ed9ru" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1expm6"></div></div><div class="child"><div id="siteTable_t1_k1expm6" class="sitetable listing"><div class=" thing noncollapsed   deleted comment " onclick="click_thing(this)" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f2t7m/" ><p class="parent"></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><em>[deleted]</em> <time title="Wed Sep 20 14:11:17 2023 UTC" datetime="2023-09-20T14:11:17+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><div class="usertext grayed"><input type="hidden" name="thing_id" value=""/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>[deleted]</p> +</div> +</div></div><ul class="flat-list buttons"></ul><div class="reportform report-t1_k1f2t7m"></div></div><div class="child"><div id="siteTable_deleted" class="sitetable listing"><div class=" thing id-t1_k1f4kto noncollapsed   comment " id="thing_t1_k1f4kto" onclick="click_thing(this)" data-fullname="t1_k1f4kto" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Myrion_Phoenix" data-author-fullname="t2_zisex" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f4kto/" ><p class="parent"><a name="k1f4kto"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Myrion_Phoenix" class="author may-blank id-t2_zisex" >Myrion_Phoenix</a><span class="userattrs"></span> <span class="score dislikes" title="7">7 points</span><span class="score unvoted" title="8">8 points</span><span class="score likes" title="9">9 points</span> <time title="Wed Sep 20 14:22:37 2023 UTC" datetime="2023-09-20T14:22:37+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f4ktohzv"><input type="hidden" name="thing_id" value="t1_k1f4kto"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>It's also helpful to follow some hashtags (which the Mastodon android app can't, but f.ex. Tusky can and the web interface also works).</p> + +<p>I follow #fido2 and #cryptography, for example, as well as stuff like #bookstodon.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f4kto/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f4kto/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f4kto"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1fvt58 noncollapsed   comment " id="thing_t1_k1fvt58" onclick="click_thing(this)" data-fullname="t1_k1fvt58" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="vitriolix" data-author-fullname="t2_38fl0" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fvt58/" ><p class="parent"><a name="k1fvt58"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/vitriolix" class="author may-blank id-t2_38fl0" >vitriolix</a><span class="userattrs"></span> <span class="score dislikes" title="14">14 points</span><span class="score unvoted" title="15">15 points</span><span class="score likes" title="16">16 points</span> <time title="Wed Sep 20 17:05:34 2023 UTC" datetime="2023-09-20T17:05:34+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fvt58i8r"><input type="hidden" name="thing_id" value="t1_k1fvt58"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>checkout the local feeds on a few instances to get lots of good content and find people to follow:</p> + +<p><a href="https://infosec.exchange/public/local">https://infosec.exchange/public/local</a></p> + +<p><a href="https://fosstodon.org/public/local">https://fosstodon.org/public/local</a></p> + +<p><a href="https://hachyderm.io/public/local">https://hachyderm.io/public/local</a></p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fvt58/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fvt58/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1expm6" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fvt58"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1fduhp noncollapsed   comment " id="thing_t1_k1fduhp" onclick="click_thing(this)" data-fullname="t1_k1fduhp" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fduhp/" ><p class="parent"><a name="k1fduhp"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><span>[deleted]</span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Wed Sep 20 15:19:46 2023 UTC" datetime="2023-09-20T15:19:46+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fduhpwm0"><input type="hidden" name="thing_id" value="t1_k1fduhp"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p><a href="https://elk.zone/infosec.exchange/@fY54DtPKe6rxMF/110757471778047861" rel="nofollow">https://elk.zone/infosec.exchange/@fY54DtPKe6rxMF/110757471778047861</a></p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fduhp/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fduhp/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1expm6" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fduhp"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1hfihx noncollapsed   comment " id="thing_t1_k1hfihx" onclick="click_thing(this)" data-fullname="t1_k1hfihx" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="mkosmo" data-author-fullname="t2_3asub" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hfihx/" ><p class="parent"><a name="k1hfihx"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/mkosmo" class="author may-blank id-t2_3asub" >mkosmo</a><span class="flairrichtext flaircolorlight flair " title="Security Architect" style="background-color: #373c3f; border-color: #373c3f;"><span>Security Architect</span></span><span class="userattrs"></span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Wed Sep 20 22:28:12 2023 UTC" datetime="2023-09-20T22:28:12+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1hfihxyb6"><input type="hidden" name="thing_id" value="t1_k1hfihx"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>The fediverse isn't nearly as populated as it's zealots would lead you to believe, unfortunately. Great concept, but just doesn't have the momentum.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hfihx/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hfihx/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1expm6" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1hfihx"></div></div><div class="child"><div id="siteTable_t1_k1hfihx" class="sitetable listing"><div class=" thing id-t1_k1kb6wr noncollapsed   comment " id="thing_t1_k1kb6wr" onclick="click_thing(this)" data-fullname="t1_k1kb6wr" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="WollCel" data-author-fullname="t2_7q0snzpw" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1kb6wr/" ><p class="parent"><a name="k1kb6wr"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/WollCel" class="author may-blank id-t2_7q0snzpw" >WollCel</a><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Thu Sep 21 13:52:01 2023 UTC" datetime="2023-09-21T13:52:01+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1kb6wre6m"><input type="hidden" name="thing_id" value="t1_k1kb6wr"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Yeah it’s growing though, it doesn’t help how ideologically splintered instances can get</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1kb6wr/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1kb6wr/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1hfihx" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1kb6wr"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1f481h noncollapsed   comment " id="thing_t1_k1f481h" onclick="click_thing(this)" data-fullname="t1_k1f481h" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Zncon" data-author-fullname="t2_hpvos" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f481h/" ><p class="parent"><a name="k1f481h"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Zncon" class="author may-blank id-t2_hpvos" >Zncon</a><span class="userattrs"></span> <span class="score dislikes" title="9">9 points</span><span class="score unvoted" title="10">10 points</span><span class="score likes" title="11">11 points</span> <time title="Wed Sep 20 14:20:24 2023 UTC" datetime="2023-09-20T14:20:24+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f481hrvj"><input type="hidden" name="thing_id" value="t1_k1f481h"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>The entire first page (and most of the rest of them) is politics and nonsense at the moment - not exactly an amazing recommendation. =/</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f481h/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f481h/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1ed9ru" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f481h"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing noncollapsed   deleted comment " onclick="click_thing(this)" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fgax9/" ><p class="parent"></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><em>[deleted]</em> <time title="Wed Sep 20 15:34:29 2023 UTC" datetime="2023-09-20T15:34:29+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><div class="usertext grayed"><input type="hidden" name="thing_id" value=""/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>[deleted]</p> +</div> +</div></div><ul class="flat-list buttons"></ul><div class="reportform report-t1_k1fgax9"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1envz7 collapsed collapsed-for-reason   comment " id="thing_t1_k1envz7" onclick="click_thing(this)" data-fullname="t1_k1envz7" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="profshmex" data-author-fullname="t2_6dja5aq1" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1envz7/" ><p class="parent"><a name="k1envz7"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[+]</a><a href="https://www.reddit.com/user/profshmex" class="author may-blank id-t2_6dja5aq1" >profshmex</a><span class="userattrs"></span> <span class="collapsed-reason">comment score below threshold</span><span class="score dislikes" title="-30">-30 points</span><span class="score unvoted" title="-29">-29 points</span><span class="score likes" title="-28">-28 points</span> <time title="Wed Sep 20 12:25:02 2023 UTC" datetime="2023-09-20T12:25:02+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(2 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1envz78qq"><input type="hidden" name="thing_id" value="t1_k1envz7"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Login creds? Nice try 😉</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1envz7/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1envz7/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1ed9ru" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1envz7"></div></div><div class="child"><div id="siteTable_t1_k1envz7" class="sitetable listing"><div class=" thing id-t1_k1f0mqa noncollapsed   comment " id="thing_t1_k1f0mqa" onclick="click_thing(this)" data-fullname="t1_k1f0mqa" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="SteveDinn" data-author-fullname="t2_n2ejp" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f0mqa/" ><p class="parent"><a name="k1f0mqa"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/SteveDinn" class="author may-blank id-t2_n2ejp" >SteveDinn</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 13:56:58 2023 UTC" datetime="2023-09-20T13:56:58+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f0mqa639"><input type="hidden" name="thing_id" value="t1_k1f0mqa"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Try <a href="https://infosec.exchange/explore" rel="nofollow">https://infosec.exchange/explore</a> instead.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f0mqa/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f0mqa/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1envz7" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f0mqa"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1f0eas noncollapsed   comment " id="thing_t1_k1f0eas" onclick="click_thing(this)" data-fullname="t1_k1f0eas" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="moker" data-author-fullname="t2_31fvu" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f0eas/" ><p class="parent"><a name="k1f0eas"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/moker" class="author may-blank id-t2_31fvu" >moker</a><span class="userattrs"></span> <span class="score dislikes" title="49">49 points</span><span class="score unvoted" title="50">50 points</span><span class="score likes" title="51">51 points</span> <time title="Wed Sep 20 13:55:24 2023 UTC" datetime="2023-09-20T13:55:24+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(8 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f0eas19s"><input type="hidden" name="thing_id" value="t1_k1f0eas"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I run <a href="https://infosec.exchange">https://infosec.exchange</a> - it has about 17000 active members, and among several other security related instances.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f0eas/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f0eas/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f0eas"></div></div><div class="child"><div id="siteTable_t1_k1f0eas" class="sitetable listing"><div class=" thing id-t1_k1f8o48 noncollapsed   comment " id="thing_t1_k1f8o48" onclick="click_thing(this)" data-fullname="t1_k1f8o48" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Elder_Meow_667" data-author-fullname="t2_ul0ujln5" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f8o48/" ><p class="parent"><a name="k1f8o48"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Elder_Meow_667" class="author may-blank id-t2_ul0ujln5" >Elder_Meow_667</a><span class="userattrs"></span> <span class="score dislikes" title="3">3 points</span><span class="score unvoted" title="4">4 points</span><span class="score likes" title="5">5 points</span> <time title="Wed Sep 20 14:48:00 2023 UTC" datetime="2023-09-20T14:48:00+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f8o48gnj"><input type="hidden" name="thing_id" value="t1_k1f8o48"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Jerrrrrry! Hehe</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f8o48/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f8o48/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f0eas" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f8o48"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1fsfsq noncollapsed   comment " id="thing_t1_k1fsfsq" onclick="click_thing(this)" data-fullname="t1_k1fsfsq" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Popka_Akoola" data-author-fullname="t2_2ym6xry6" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fsfsq/" ><p class="parent"><a name="k1fsfsq"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Popka_Akoola" class="author may-blank id-t2_2ym6xry6" >Popka_Akoola</a><span class="userattrs"></span> <span class="score dislikes" title="3">3 points</span><span class="score unvoted" title="4">4 points</span><span class="score likes" title="5">5 points</span> <time title="Wed Sep 20 16:45:58 2023 UTC" datetime="2023-09-20T16:45:58+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fsfsqxhm"><input type="hidden" name="thing_id" value="t1_k1fsfsq"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>May get downvoted for this but having been one of the earlier adopters into mastodon and infosec exchange specifically, I just don't see how it's better than X. I get a lot of crazy things have happened, but 95% of posts I see on infosec exchange are people congratulating themselves and being so proud they left Twitter and the other 5% are people introducing themselves and talking about their day/treating the platform like Twitter.</p> + +<p>I love the idea of Mastodon in general and I have high hopes for it's future, but I really think people are deluding themselves if they say it has better content at the moment.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fsfsq/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fsfsq/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f0eas" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fsfsq"></div></div><div class="child"><div id="siteTable_t1_k1fsfsq" class="sitetable listing"><div class=" thing id-t1_k1ibs3x noncollapsed   comment " id="thing_t1_k1ibs3x" onclick="click_thing(this)" data-fullname="t1_k1ibs3x" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="syn-ack-fin" data-author-fullname="t2_6nnp3" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ibs3x/" ><p class="parent"><a name="k1ibs3x"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/syn-ack-fin" class="author may-blank id-t2_6nnp3" >syn-ack-fin</a><span class="userattrs"></span> <span class="score dislikes" title="4">4 points</span><span class="score unvoted" title="5">5 points</span><span class="score likes" title="6">6 points</span> <time title="Thu Sep 21 02:11:56 2023 UTC" datetime="2023-09-21T02:11:56+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1ibs3xlez"><input type="hidden" name="thing_id" value="t1_k1ibs3x"/><div class="usertext-body may-blank-within md-container " ><div class="md"><blockquote> +<p>I just don’t see how it’s better than X</p> +</blockquote> + +<p>Better is obviously relative, but Mastodon does take more work. The end result is that you have a feed solely with the information you want and not what is pushed on you. Oh and fewer nazis is nice too.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ibs3x/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ibs3x/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1fsfsq" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1ibs3x"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1foxtg noncollapsed   controversial comment " id="thing_t1_k1foxtg" onclick="click_thing(this)" data-fullname="t1_k1foxtg" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Fallingdamage" data-author-fullname="t2_44txb" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1foxtg/" ><p class="parent"><a name="k1foxtg"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Fallingdamage" class="author may-blank id-t2_44txb" >Fallingdamage</a><span class="userattrs"></span> <span class="score dislikes" title="4">4 points</span><span class="score unvoted" title="5">5 points</span><span class="score likes" title="6">6 points</span> <time title="Wed Sep 20 16:25:25 2023 UTC" datetime="2023-09-20T16:25:25+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(3 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1foxtgp8a"><input type="hidden" name="thing_id" value="t1_k1foxtg"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Just followed link. Bunch of political posts, ice cream stands and star trek jokes. I think I get better content on <a href="/r/cybersecurity">r/cybersecurity</a> and arstechnica</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1foxtg/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1foxtg/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f0eas" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1foxtg"></div></div><div class="child"><div id="siteTable_t1_k1foxtg" class="sitetable listing"><div class=" thing id-t1_k1fqxd3 noncollapsed   comment " id="thing_t1_k1fqxd3" onclick="click_thing(this)" data-fullname="t1_k1fqxd3" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="moker" data-author-fullname="t2_31fvu" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fqxd3/" ><p class="parent"><a name="k1fqxd3"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/moker" class="author may-blank id-t2_31fvu" >moker</a><span class="userattrs"></span> <span class="score dislikes" title="4">4 points</span><span class="score unvoted" title="5">5 points</span><span class="score likes" title="6">6 points</span> <time title="Wed Sep 20 16:37:04 2023 UTC" datetime="2023-09-20T16:37:04+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(2 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fqxd39lu"><input type="hidden" name="thing_id" value="t1_k1fqxd3"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I don't think you were looking at the correct timeline - this is more representative of what we see: <a href="https://infosec.exchange/public/local">https://infosec.exchange/public/local</a></p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fqxd3/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fqxd3/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1foxtg" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fqxd3"></div></div><div class="child"><div id="siteTable_t1_k1fqxd3" class="sitetable listing"><div class=" thing noncollapsed   deleted controversial comment " onclick="click_thing(this)" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fyl9y/" ><p class="parent"></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><em>[deleted]</em> <time title="Wed Sep 20 17:21:40 2023 UTC" datetime="2023-09-20T17:21:40+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><div class="usertext grayed"><input type="hidden" name="thing_id" value=""/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>[deleted]</p> +</div> +</div></div><ul class="flat-list buttons"></ul><div class="reportform report-t1_k1fyl9y"></div></div><div class="child"><div id="siteTable_deleted" class="sitetable listing"><div class=" thing id-t1_k1fzrwf noncollapsed   comment " id="thing_t1_k1fzrwf" onclick="click_thing(this)" data-fullname="t1_k1fzrwf" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="moker" data-author-fullname="t2_31fvu" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fzrwf/" ><p class="parent"><a name="k1fzrwf"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/moker" class="author may-blank id-t2_31fvu" >moker</a><span class="userattrs"></span> <span class="score dislikes" title="7">7 points</span><span class="score unvoted" title="8">8 points</span><span class="score likes" title="9">9 points</span> <time title="Wed Sep 20 17:28:28 2023 UTC" datetime="2023-09-20T17:28:28+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fzrwf2yq"><input type="hidden" name="thing_id" value="t1_k1fzrwf"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Thanks for giving it a look.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fzrwf/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fzrwf/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fzrwf"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1fmsen noncollapsed   comment " id="thing_t1_k1fmsen" onclick="click_thing(this)" data-fullname="t1_k1fmsen" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Individual-Ad-9902" data-author-fullname="t2_75h3ss5c" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fmsen/" ><p class="parent"><a name="k1fmsen"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Individual-Ad-9902" class="author may-blank id-t2_75h3ss5c" >Individual-Ad-9902</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 16:12:49 2023 UTC" datetime="2023-09-20T16:12:49+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fmsen3eo"><input type="hidden" name="thing_id" value="t1_k1fmsen"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>SECOND!</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fmsen/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fmsen/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f0eas" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fmsen"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1ee5j5 noncollapsed   comment " id="thing_t1_k1ee5j5" onclick="click_thing(this)" data-fullname="t1_k1ee5j5" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="TradeApe" data-author-fullname="t2_51lfc8p8" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ee5j5/" ><p class="parent"><a name="k1ee5j5"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/TradeApe" class="author may-blank id-t2_51lfc8p8" >TradeApe</a><span class="userattrs"></span> <span class="score dislikes" title="37">37 points</span><span class="score unvoted" title="38">38 points</span><span class="score likes" title="39">39 points</span> <time title="Wed Sep 20 10:58:41 2023 UTC" datetime="2023-09-20T10:58:41+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1ee5j50pl"><input type="hidden" name="thing_id" value="t1_k1ee5j5"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Infosec.exchange mastodon server</p> + +<p>X feels too much like Rumble or an Alex Jones fan club with the content that gets pushed. Definitely not paying for that.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ee5j5/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ee5j5/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1ee5j5"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1ek11e noncollapsed   comment " id="thing_t1_k1ek11e" onclick="click_thing(this)" data-fullname="t1_k1ek11e" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Doc_Hobb" data-author-fullname="t2_5f8eu" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ek11e/" ><p class="parent"><a name="k1ek11e"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Doc_Hobb" class="author may-blank id-t2_5f8eu" >Doc_Hobb</a><span class="flairrichtext flaircolorlight flair " title="Vulnerability Researcher" style="background-color: #373c3f; border-color: #373c3f;"><span>Vulnerability Researcher</span></span><span class="userattrs"></span> <span class="score dislikes" title="17">17 points</span><span class="score unvoted" title="18">18 points</span><span class="score likes" title="19">19 points</span> <time title="Wed Sep 20 11:53:33 2023 UTC" datetime="2023-09-20T11:53:33+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1ek11e6rl"><input type="hidden" name="thing_id" value="t1_k1ek11e"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I like to use <a href="https://allinfosecnews.com">https://allinfosecnews.com</a> it’s a great collection of feeds</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ek11e/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ek11e/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1ek11e"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1he78h noncollapsed   comment " id="thing_t1_k1he78h" onclick="click_thing(this)" data-fullname="t1_k1he78h" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Rebootkid" data-author-fullname="t2_4fhi9" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1he78h/" ><p class="parent"><a name="k1he78h"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Rebootkid" class="author may-blank id-t2_4fhi9" >Rebootkid</a><span class="userattrs"></span> <span class="score dislikes" title="4">4 points</span><span class="score unvoted" title="5">5 points</span><span class="score likes" title="6">6 points</span> <time title="Wed Sep 20 22:19:37 2023 UTC" datetime="2023-09-20T22:19:37+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1he78hf0b"><input type="hidden" name="thing_id" value="t1_k1he78h"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I'm over on infosec.exchange. Found it very useful.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1he78h/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1he78h/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1he78h"></div></div><div class="child"><div id="siteTable_t1_k1he78h" class="sitetable listing"><div class=" thing id-t1_k1i0icy noncollapsed   comment " id="thing_t1_k1i0icy" onclick="click_thing(this)" data-fullname="t1_k1i0icy" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="hudsoncress" data-author-fullname="t2_18vpapcs" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1i0icy/" ><p class="parent"><a name="k1i0icy"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/hudsoncress" class="author may-blank id-t2_18vpapcs" >hudsoncress</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Thu Sep 21 00:52:19 2023 UTC" datetime="2023-09-21T00:52:19+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1i0icygih"><input type="hidden" name="thing_id" value="t1_k1i0icy"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>This is the way</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1i0icy/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1i0icy/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1he78h" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1i0icy"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1erfy3 noncollapsed   comment " id="thing_t1_k1erfy3" onclick="click_thing(this)" data-fullname="t1_k1erfy3" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1erfy3/" ><p class="parent"><a name="k1erfy3"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><span>[deleted]</span> <span class="score dislikes" title="10">10 points</span><span class="score unvoted" title="11">11 points</span><span class="score likes" title="12">12 points</span> <time title="Wed Sep 20 12:52:21 2023 UTC" datetime="2023-09-20T12:52:21+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(3 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1erfy37b8"><input type="hidden" name="thing_id" value="t1_k1erfy3"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>What I find interesting is after I closed my account, over time I realized I was gradually being linked to Twitter less and less by other external websites/ news sites. Now weeks can go by without it happening. </p> + +<p>So if his goal is to make twitter no longer relevant, he's doing a banger job.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1erfy3/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1erfy3/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1erfy3"></div></div><div class="child"><div id="siteTable_t1_k1erfy3" class="sitetable listing"><div class=" thing id-t1_k1f72xa noncollapsed   comment " id="thing_t1_k1f72xa" onclick="click_thing(this)" data-fullname="t1_k1f72xa" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="itwasaraccoon" data-author-fullname="t2_tyqigoko" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f72xa/" ><p class="parent"><a name="k1f72xa"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/itwasaraccoon" class="author may-blank id-t2_tyqigoko" >itwasaraccoon</a><span class="userattrs"></span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Wed Sep 20 14:38:09 2023 UTC" datetime="2023-09-20T14:38:09+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(2 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f72xa4h4"><input type="hidden" name="thing_id" value="t1_k1f72xa"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Same with me. But I have to admit that the huge security community and information exchange on Twitter used to be super helpful to stay up to date. Its going to take a long time to replicate that somewhere else.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f72xa/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f72xa/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1erfy3" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f72xa"></div></div><div class="child"><div id="siteTable_t1_k1f72xa" class="sitetable listing"><div class=" thing id-t1_k1fz12d noncollapsed   comment " id="thing_t1_k1fz12d" onclick="click_thing(this)" data-fullname="t1_k1fz12d" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="650REDHAIR" data-author-fullname="t2_18ip5p" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fz12d/" ><p class="parent"><a name="k1fz12d"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/650REDHAIR" class="author may-blank id-t2_18ip5p" >650REDHAIR</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 17:24:11 2023 UTC" datetime="2023-09-20T17:24:11+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fz12dbgj"><input type="hidden" name="thing_id" value="t1_k1fz12d"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>My tinfoil hat might be on too tight, but sometimes I wonder if that is by design.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fz12d/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fz12d/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f72xa" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fz12d"></div></div><div class="child"><div id="siteTable_t1_k1fz12d" class="sitetable listing"><div class=" thing id-t1_k1g1jnw noncollapsed   comment " id="thing_t1_k1g1jnw" onclick="click_thing(this)" data-fullname="t1_k1g1jnw" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="itwasaraccoon" data-author-fullname="t2_tyqigoko" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1g1jnw/" ><p class="parent"><a name="k1g1jnw"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/itwasaraccoon" class="author may-blank id-t2_tyqigoko" >itwasaraccoon</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 17:38:42 2023 UTC" datetime="2023-09-20T17:38:42+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1g1jnwdk1"><input type="hidden" name="thing_id" value="t1_k1g1jnw"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Have you tried the new Titanium hat instead? People seem to love the color at least.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1g1jnw/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1g1jnw/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1fz12d" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1g1jnw"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1dztic noncollapsed   comment " id="thing_t1_k1dztic" onclick="click_thing(this)" data-fullname="t1_k1dztic" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="AyeSocketFucker" data-author-fullname="t2_9dx76" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1dztic/" ><p class="parent"><a name="k1dztic"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/AyeSocketFucker" class="author may-blank id-t2_9dx76" >AyeSocketFucker</a><span class="userattrs"></span> <span class="score dislikes" title="8">8 points</span><span class="score unvoted" title="9">9 points</span><span class="score likes" title="10">10 points</span> <time title="Wed Sep 20 07:58:53 2023 UTC" datetime="2023-09-20T07:58:53+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(4 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1dzticb63"><input type="hidden" name="thing_id" value="t1_k1dztic"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>It was mastodon, not sure anymore, haven’t used it in awhile</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1dztic/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1dztic/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1dztic"></div></div><div class="child"><div id="siteTable_t1_k1dztic" class="sitetable listing"><div class=" thing id-t1_k1fwapb noncollapsed   comment " id="thing_t1_k1fwapb" onclick="click_thing(this)" data-fullname="t1_k1fwapb" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="vitriolix" data-author-fullname="t2_38fl0" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fwapb/" ><p class="parent"><a name="k1fwapb"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/vitriolix" class="author may-blank id-t2_38fl0" >vitriolix</a><span class="userattrs"></span> <span class="score dislikes" title="5">5 points</span><span class="score unvoted" title="6">6 points</span><span class="score likes" title="7">7 points</span> <time title="Wed Sep 20 17:08:24 2023 UTC" datetime="2023-09-20T17:08:24+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(3 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fwapbn9u"><input type="hidden" name="thing_id" value="t1_k1fwapb"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Masto is thriving and growing rapidly, up to 14mil created accounts now (and of course lower monthly actives, but still very active). Every time there is news of more twitter stupidity there is a new spike of signups</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fwapb/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fwapb/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1dztic" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fwapb"></div></div><div class="child"><div id="siteTable_t1_k1fwapb" class="sitetable listing"><div class=" thing id-t1_k1h0z54 noncollapsed   comment " id="thing_t1_k1h0z54" onclick="click_thing(this)" data-fullname="t1_k1h0z54" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h0z54/" ><p class="parent"><a name="k1h0z54"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><span>[deleted]</span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 20:59:56 2023 UTC" datetime="2023-09-20T20:59:56+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(2 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1h0z54yqx"><input type="hidden" name="thing_id" value="t1_k1h0z54"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I tried Mastadon but I found it very difficult to actually see any posts that were actually worthwhile or interesting. It seems their algorithms for content recommendation need a lot of work or don't exist.</p> + +<p>I don't care about who posted most recently, I want to know what's actually worth reading that day.</p> + +<p>Also tried Threads but found it difficult to even find the content I wanted to see.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h0z54/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h0z54/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1fwapb" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1h0z54"></div></div><div class="child"><div id="siteTable_t1_k1h0z54" class="sitetable listing"><div class=" thing id-t1_k1hfmcg noncollapsed   comment " id="thing_t1_k1hfmcg" onclick="click_thing(this)" data-fullname="t1_k1hfmcg" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="coloRD" data-author-fullname="t2_8d2bp" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hfmcg/" ><p class="parent"><a name="k1hfmcg"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/coloRD" class="author may-blank id-t2_8d2bp" >coloRD</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 22:28:54 2023 UTC" datetime="2023-09-20T22:28:54+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1hfmcg501"><input type="hidden" name="thing_id" value="t1_k1hfmcg"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>it is based more on hashtags and you choosing who to follow than recommender algorithms. In fact many mastodon users often proudly proclaim they do not want to live in an algorithmically generated bubble being fed content.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hfmcg/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hfmcg/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1h0z54" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1hfmcg"></div></div><div class="child"><div id="siteTable_t1_k1hfmcg" class="sitetable listing"><div class=" thing id-t1_k1jhi39 noncollapsed   comment " id="thing_t1_k1jhi39" onclick="click_thing(this)" data-fullname="t1_k1jhi39" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1jhi39/" ><p class="parent"><a name="k1jhi39"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><span>[deleted]</span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Thu Sep 21 09:27:22 2023 UTC" datetime="2023-09-21T09:27:22+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1jhi39r43"><input type="hidden" name="thing_id" value="t1_k1jhi39"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>It is a double edged sword though as it makes it more difficult to find that content you want to follow - I don't think algorithms are inherently bad as long as they can't be manipulated.</p> + +<p>That being said, Reddit mostly managed without algorithms thanks to voting & community driven recommendations, but Mastadon doesn't have that.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1jhi39/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1jhi39/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1hfmcg" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1jhi39"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1h5u7u noncollapsed   comment " id="thing_t1_k1h5u7u" onclick="click_thing(this)" data-fullname="t1_k1h5u7u" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="AnIrregularRegular" data-author-fullname="t2_164qs4" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h5u7u/" ><p class="parent"><a name="k1h5u7u"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/AnIrregularRegular" class="author may-blank id-t2_164qs4" >AnIrregularRegular</a><span class="flairrichtext flaircolorlight flair " title="Blue Team" style="background-color: #373c3f; border-color: #373c3f;"><span>Blue Team</span></span><span class="userattrs"></span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Wed Sep 20 21:28:06 2023 UTC" datetime="2023-09-20T21:28:06+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1h5u7ub0u"><input type="hidden" name="thing_id" value="t1_k1h5u7u"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Honestly I’ve yet to run into a great Twitter replacement. Honestly Reddit is maybe the best for stuff before it hits blogs/news which is why I loved Twitter. Go follow sysadmin and MSP and they often see stuff before security people do.</p> + +<p>Mastodon is alright but just didn’t scratch the itch the same(also like to follow a lot of foreign policy/natsec peeps who won’t do Mastodon).</p> + +<p>Recently got into Bluesky and it’s okay. Not Twitter but also I think has a lot of potential.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h5u7u/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h5u7u/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1h5u7u"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1hk3lb noncollapsed   comment " id="thing_t1_k1hk3lb" onclick="click_thing(this)" data-fullname="t1_k1hk3lb" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="LordCommanderTaurusG" data-author-fullname="t2_v693a42" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hk3lb/" ><p class="parent"><a name="k1hk3lb"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/LordCommanderTaurusG" class="author may-blank id-t2_v693a42" >LordCommanderTaurusG</a><span class="flairrichtext flaircolorlight flair " title="Blue Team" style="background-color: #373c3f; border-color: #373c3f;"><span>Blue Team</span></span><span class="userattrs"></span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Wed Sep 20 22:59:01 2023 UTC" datetime="2023-09-20T22:59:01+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1hk3lbwhi"><input type="hidden" name="thing_id" value="t1_k1hk3lb"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Have you tried Threads?</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hk3lb/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hk3lb/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1hk3lb"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1hvox4 noncollapsed   comment " id="thing_t1_k1hvox4" onclick="click_thing(this)" data-fullname="t1_k1hvox4" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Tetmohawk" data-author-fullname="t2_2x2zoj2" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hvox4/" ><p class="parent"><a name="k1hvox4"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Tetmohawk" class="author may-blank id-t2_2x2zoj2" >Tetmohawk</a><span class="userattrs"></span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Thu Sep 21 00:19:26 2023 UTC" datetime="2023-09-21T00:19:26+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1hvox4c5j"><input type="hidden" name="thing_id" value="t1_k1hvox4"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I see no ads. Pretty close to never. Here's how:</p> + +<p>(1) Use a DNS filter like CleanBrowsing.com and set it to filter ads and tracking.</p> + +<p>(2) Put Twitter in its own container. You can do this easily in Firefox. That way cookies and other stuff related to ads is isolated from every other website. </p> + +<p>(3) Use a Firefox add-on. I use both Privacy badger and uBlock origin.</p> + +<p>I don't see ads on almost any site with this method. At work I get ads all the time and it's annoying. Not sure how y'all lived like this for so long. Ads haven't been a part of my life for years. Now you know why.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hvox4/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hvox4/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1hvox4"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1eii4j noncollapsed   comment " id="thing_t1_k1eii4j" onclick="click_thing(this)" data-fullname="t1_k1eii4j" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="mobo_dojo" data-author-fullname="t2_70dgodgm" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eii4j/" ><p class="parent"><a name="k1eii4j"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/mobo_dojo" class="author may-blank id-t2_70dgodgm" >mobo_dojo</a><span class="userattrs"></span> <span class="score dislikes" title="4">4 points</span><span class="score unvoted" title="5">5 points</span><span class="score likes" title="6">6 points</span> <time title="Wed Sep 20 11:40:21 2023 UTC" datetime="2023-09-20T11:40:21+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1eii4j0n0"><input type="hidden" name="thing_id" value="t1_k1eii4j"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Newsboat</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eii4j/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eii4j/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1eii4j"></div></div><div class="child"><div id="siteTable_t1_k1eii4j" class="sitetable listing"><div class=" thing id-t1_k1ftwx9 noncollapsed   comment " id="thing_t1_k1ftwx9" onclick="click_thing(this)" data-fullname="t1_k1ftwx9" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="irkine" data-author-fullname="t2_katin" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ftwx9/" ><p class="parent"><a name="k1ftwx9"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/irkine" class="author may-blank id-t2_katin" >irkine</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 16:54:33 2023 UTC" datetime="2023-09-20T16:54:33+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1ftwx9dzw"><input type="hidden" name="thing_id" value="t1_k1ftwx9"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>What does your feed list look like for security? :)</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ftwx9/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ftwx9/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1eii4j" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1ftwx9"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1fdb6p noncollapsed   comment " id="thing_t1_k1fdb6p" onclick="click_thing(this)" data-fullname="t1_k1fdb6p" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Versed_Percepton" data-author-fullname="t2_94cm2jqqy" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fdb6p/" ><p class="parent"><a name="k1fdb6p"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Versed_Percepton" class="author may-blank id-t2_94cm2jqqy" >Versed_Percepton</a><span class="userattrs"></span> <span class="score dislikes" title="3">3 points</span><span class="score unvoted" title="4">4 points</span><span class="score likes" title="5">5 points</span> <time title="Wed Sep 20 15:16:29 2023 UTC" datetime="2023-09-20T15:16:29+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fdb6pxm7"><input type="hidden" name="thing_id" value="t1_k1fdb6p"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>The closest thing to Twitter would be mastodon right now. You just need to decide on your home instance, build your profile like you would anywhere else, and start finding topics, hashtags, and people/groups to follow. Then filter out the junk(you can black list on keywords) so you can rebuild your RSS like you have it setup on Twitter. </p> + +<p>There are a dozen or so Infosec instances to choose from, I like <a href="https://Infosec.Exchange" rel="nofollow">Infosec.Exchange</a> as its stable and a smooth experience. It has a solid Admin team and has no issues talking to the federation. The membership on the instance is pretty open and welcoming.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fdb6p/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fdb6p/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fdb6p"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1fn2i6 noncollapsed   comment " id="thing_t1_k1fn2i6" onclick="click_thing(this)" data-fullname="t1_k1fn2i6" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Individual-Ad-9902" data-author-fullname="t2_75h3ss5c" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fn2i6/" ><p class="parent"><a name="k1fn2i6"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Individual-Ad-9902" class="author may-blank id-t2_75h3ss5c" >Individual-Ad-9902</a><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Wed Sep 20 16:14:27 2023 UTC" datetime="2023-09-20T16:14:27+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fn2i6urt"><input type="hidden" name="thing_id" value="t1_k1fn2i6"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Infosec.exchange on Mastodon is a very good place, and I get a lot of good information from my curated group on Linkedin. Dr. Chase Cunningham has a good weekly wrap up. And then there is always Cyber Protection Magazine.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fn2i6/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fn2i6/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fn2i6"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1encn2 noncollapsed   comment " id="thing_t1_k1encn2" onclick="click_thing(this)" data-fullname="t1_k1encn2" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="eat_the_pennies" data-author-fullname="t2_vrscons4" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1encn2/" ><p class="parent"><a name="k1encn2"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/eat_the_pennies" class="author may-blank id-t2_vrscons4" >eat_the_pennies</a><span class="flairrichtext flaircolorlight flair " title="System Administrator" style="background-color: #373c3f; border-color: #373c3f;"><span>System Administrator</span></span><span class="userattrs"></span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Wed Sep 20 12:20:49 2023 UTC" datetime="2023-09-20T12:20:49+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(7 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1encn2edi"><input type="hidden" name="thing_id" value="t1_k1encn2"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I'm hoping Bluesky gets more popular once it actually opens. I was able to join yesterday and there's a small community of infosec people who share news there.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1encn2/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1encn2/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1encn2"></div></div><div class="child"><div id="siteTable_t1_k1encn2" class="sitetable listing"><div class=" thing id-t1_k1f55vs noncollapsed   comment " id="thing_t1_k1f55vs" onclick="click_thing(this)" data-fullname="t1_k1f55vs" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="SpaceMaxil" data-author-fullname="t2_iw8rdehp" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f55vs/" ><p class="parent"><a name="k1f55vs"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/SpaceMaxil" class="author may-blank id-t2_iw8rdehp" >SpaceMaxil</a><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Wed Sep 20 14:26:15 2023 UTC" datetime="2023-09-20T14:26:15+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(4 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f55vsbw3"><input type="hidden" name="thing_id" value="t1_k1f55vs"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Seems the chatty security folks are pretty split on Mastadon vs BlueSky. But most of the good leaks still end up on Twitter first.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f55vs/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f55vs/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1encn2" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f55vs"></div></div><div class="child"><div id="siteTable_t1_k1f55vs" class="sitetable listing"><div class=" thing id-t1_k1f6e6p noncollapsed   comment " id="thing_t1_k1f6e6p" onclick="click_thing(this)" data-fullname="t1_k1f6e6p" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="eat_the_pennies" data-author-fullname="t2_vrscons4" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f6e6p/" ><p class="parent"><a name="k1f6e6p"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/eat_the_pennies" class="author may-blank id-t2_vrscons4" >eat_the_pennies</a><span class="flairrichtext flaircolorlight flair " title="System Administrator" style="background-color: #373c3f; border-color: #373c3f;"><span>System Administrator</span></span><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Wed Sep 20 14:33:58 2023 UTC" datetime="2023-09-20T14:33:58+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(3 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f6e6phz7"><input type="hidden" name="thing_id" value="t1_k1f6e6p"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Mastodon would've taken off by now if it really was ideal imo. The hesitancy leads me to believe people are really holding out for Bluesky to be Twitter 2.0</p> + +<p>Who knows if we'll ever get to that point though</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f6e6p/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f6e6p/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f55vs" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f6e6p"></div></div><div class="child"><div id="siteTable_t1_k1f6e6p" class="sitetable listing"><div class=" thing id-t1_k1f743s noncollapsed   comment " id="thing_t1_k1f743s" onclick="click_thing(this)" data-fullname="t1_k1f743s" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="SpaceMaxil" data-author-fullname="t2_iw8rdehp" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f743s/" ><p class="parent"><a name="k1f743s"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/SpaceMaxil" class="author may-blank id-t2_iw8rdehp" >SpaceMaxil</a><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Wed Sep 20 14:38:21 2023 UTC" datetime="2023-09-20T14:38:21+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f743sn6p"><input type="hidden" name="thing_id" value="t1_k1f743s"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Folks are also looking for apps that work across fediverses. Seems to have potential.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f743s/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f743s/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f6e6p" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f743s"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1flu07 noncollapsed   comment " id="thing_t1_k1flu07" onclick="click_thing(this)" data-fullname="t1_k1flu07" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="look_ima_frog" data-author-fullname="t2_hbyptyxz" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1flu07/" ><p class="parent"><a name="k1flu07"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/look_ima_frog" class="author may-blank id-t2_hbyptyxz" >look_ima_frog</a><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Wed Sep 20 16:07:07 2023 UTC" datetime="2023-09-20T16:07:07+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1flu07ulq"><input type="hidden" name="thing_id" value="t1_k1flu07"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I'm like a lot of people who have tried mastodon and nope out. </p> + +<p>I get the idea, but I still don't care to use it. User experience is not good. I have enough to learn and fix for my work, dealing with an overwrought platform isn't on the list right now.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1flu07/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1flu07/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f6e6p" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1flu07"></div></div><div class="child"><div id="siteTable_t1_k1flu07" class="sitetable listing"><div class=" thing id-t1_k1gstv3 noncollapsed   comment " id="thing_t1_k1gstv3" onclick="click_thing(this)" data-fullname="t1_k1gstv3" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Slythela" data-author-fullname="t2_ixk2h" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gstv3/" ><p class="parent"><a name="k1gstv3"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Slythela" class="author may-blank id-t2_ixk2h" >Slythela</a><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Wed Sep 20 20:12:56 2023 UTC" datetime="2023-09-20T20:12:56+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1gstv3860"><input type="hidden" name="thing_id" value="t1_k1gstv3"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I'm the same way. I was pretty pumped to have another platform, I'm pretty over this website and I've never been into twitter. It's just not really there yet though, and it feels more like a facebook feed than anything else, even on the infosec ones.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gstv3/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gstv3/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1flu07" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1gstv3"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1ewj8c noncollapsed   comment " id="thing_t1_k1ewj8c" onclick="click_thing(this)" data-fullname="t1_k1ewj8c" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="flylikegaruda" data-author-fullname="t2_1189kv" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ewj8c/" ><p class="parent"><a name="k1ewj8c"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/flylikegaruda" class="author may-blank id-t2_1189kv" >flylikegaruda</a><span class="flairrichtext flaircolorlight flair " title="Red Team" style="background-color: #373c3f; border-color: #373c3f;"><span>Red Team</span></span><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 13:29:00 2023 UTC" datetime="2023-09-20T13:29:00+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1ewj8c5y7"><input type="hidden" name="thing_id" value="t1_k1ewj8c"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Yes, but I get more cat pics than security. I am no fan of cats!</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ewj8c/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ewj8c/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1encn2" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1ewj8c"></div></div><div class="child"><div id="siteTable_t1_k1ewj8c" class="sitetable listing"><div class=" thing id-t1_k1f3pc2 noncollapsed   comment " id="thing_t1_k1f3pc2" onclick="click_thing(this)" data-fullname="t1_k1f3pc2" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="eat_the_pennies" data-author-fullname="t2_vrscons4" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f3pc2/" ><p class="parent"><a name="k1f3pc2"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/eat_the_pennies" class="author may-blank id-t2_vrscons4" >eat_the_pennies</a><span class="flairrichtext flaircolorlight flair " title="System Administrator" style="background-color: #373c3f; border-color: #373c3f;"><span>System Administrator</span></span><span class="userattrs"></span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Wed Sep 20 14:17:05 2023 UTC" datetime="2023-09-20T14:17:05+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f3pc255m"><input type="hidden" name="thing_id" value="t1_k1f3pc2"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Unfortunate, cats are a huge part of my life :)</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f3pc2/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f3pc2/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1ewj8c" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f3pc2"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1e9gx9 noncollapsed   controversial comment " id="thing_t1_k1e9gx9" onclick="click_thing(this)" data-fullname="t1_k1e9gx9" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="asecuredlife" data-author-fullname="t2_57r4boy9" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1e9gx9/" ><p class="parent"><a name="k1e9gx9"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/asecuredlife" class="author may-blank id-t2_57r4boy9" >asecuredlife</a><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Wed Sep 20 10:05:13 2023 UTC" datetime="2023-09-20T10:05:13+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(4 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1e9gx9755"><input type="hidden" name="thing_id" value="t1_k1e9gx9"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Weirder? Twitter has always been a weird place.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1e9gx9/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1e9gx9/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1e9gx9"></div></div><div class="child"><div id="siteTable_t1_k1e9gx9" class="sitetable listing"><div class=" thing id-t1_k1eb9de noncollapsed   comment " id="thing_t1_k1eb9de" onclick="click_thing(this)" data-fullname="t1_k1eb9de" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="skeedooshski" data-author-fullname="t2_2w3lp8yl" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eb9de/" ><p class="parent"><a name="k1eb9de"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/skeedooshski" class="author submitter may-blank id-t2_2w3lp8yl" >skeedooshski</a><span class="userattrs">[<a class="submitter" title="submitter" href="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/">S</a>]</span> <span class="score dislikes" title="20">20 points</span><span class="score unvoted" title="21">21 points</span><span class="score likes" title="22">22 points</span> <time title="Wed Sep 20 10:26:28 2023 UTC" datetime="2023-09-20T10:26:28+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1eb9dedum"><input type="hidden" name="thing_id" value="t1_k1eb9de"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Hence the weirdER :). Initially a part of its appeal, but increasingly not the case as of lately.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eb9de/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eb9de/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1e9gx9" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1eb9de"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1eszi4 noncollapsed   comment " id="thing_t1_k1eszi4" onclick="click_thing(this)" data-fullname="t1_k1eszi4" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="ComfortableProperty9" data-author-fullname="t2_3s11dgpn" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eszi4/" ><p class="parent"><a name="k1eszi4"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/ComfortableProperty9" class="author may-blank id-t2_3s11dgpn" >ComfortableProperty9</a><span class="userattrs"></span> <span class="score dislikes" title="5">5 points</span><span class="score unvoted" title="6">6 points</span><span class="score likes" title="7">7 points</span> <time title="Wed Sep 20 13:03:51 2023 UTC" datetime="2023-09-20T13:03:51+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1eszi4s4l"><input type="hidden" name="thing_id" value="t1_k1eszi4"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>It went to shit right as I got my feed cultivated exactly like I wanted it.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eszi4/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eszi4/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1e9gx9" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1eszi4"></div></div><div class="child"><div id="siteTable_t1_k1eszi4" class="sitetable listing"><div class=" thing id-t1_k1eubmi noncollapsed   comment " id="thing_t1_k1eubmi" onclick="click_thing(this)" data-fullname="t1_k1eubmi" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="missed_sla" data-author-fullname="t2_1pw5guwr" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eubmi/" ><p class="parent"><a name="k1eubmi"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/missed_sla" class="author may-blank id-t2_1pw5guwr" >missed_sla</a><span class="userattrs"></span> <span class="score dislikes" title="7">7 points</span><span class="score unvoted" title="8">8 points</span><span class="score likes" title="9">9 points</span> <time title="Wed Sep 20 13:13:34 2023 UTC" datetime="2023-09-20T13:13:34+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1eubmisqg"><input type="hidden" name="thing_id" value="t1_k1eubmi"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Twitter is a hydra of insane conservatives now. Block one and 5 more are shoved into your face. My block list hundreds long and growing every time I load up that goddamn website. Honestly don't know why I do anymore, it's usually about 30 seconds before I close it again.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eubmi/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eubmi/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1eszi4" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1eubmi"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1ewh6y noncollapsed   comment " id="thing_t1_k1ewh6y" onclick="click_thing(this)" data-fullname="t1_k1ewh6y" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="WummageSail" data-author-fullname="t2_14ajti" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ewh6y/" ><p class="parent"><a name="k1ewh6y"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/WummageSail" class="author may-blank id-t2_14ajti" >WummageSail</a><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Wed Sep 20 13:28:36 2023 UTC" datetime="2023-09-20T13:28:36+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1ewh6yuvf"><input type="hidden" name="thing_id" value="t1_k1ewh6y"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p><a href="https://risky.biz/" rel="nofollow">https://risky.biz/</a> podcast</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ewh6y/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ewh6y/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1ewh6y"></div></div><div class="child"><div id="siteTable_t1_k1ewh6y" class="sitetable listing"><div class=" thing id-t1_k1fhicr noncollapsed   comment " id="thing_t1_k1fhicr" onclick="click_thing(this)" data-fullname="t1_k1fhicr" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="skeedooshski" data-author-fullname="t2_2w3lp8yl" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fhicr/" ><p class="parent"><a name="k1fhicr"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/skeedooshski" class="author submitter may-blank id-t2_2w3lp8yl" >skeedooshski</a><span class="userattrs">[<a class="submitter" title="submitter" href="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/">S</a>]</span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 15:41:40 2023 UTC" datetime="2023-09-20T15:41:40+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fhicruwc"><input type="hidden" name="thing_id" value="t1_k1fhicr"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>It is awesome</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fhicr/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fhicr/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1ewh6y" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fhicr"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1firy2 noncollapsed   comment " id="thing_t1_k1firy2" onclick="click_thing(this)" data-fullname="t1_k1firy2" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Maidentyone" data-author-fullname="t2_ampb5p3" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1firy2/" ><p class="parent"><a name="k1firy2"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Maidentyone" class="author may-blank id-t2_ampb5p3" >Maidentyone</a><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Wed Sep 20 15:49:07 2023 UTC" datetime="2023-09-20T15:49:07+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(2 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1firy2u0d"><input type="hidden" name="thing_id" value="t1_k1firy2"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I use Feedly it has excellent security feed, plus you can add your own (rss) feeds</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1firy2/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1firy2/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1firy2"></div></div><div class="child"><div id="siteTable_t1_k1firy2" class="sitetable listing"><div class=" thing id-t1_k1fopgv noncollapsed   comment " id="thing_t1_k1fopgv" onclick="click_thing(this)" data-fullname="t1_k1fopgv" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="skeedooshski" data-author-fullname="t2_2w3lp8yl" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fopgv/" ><p class="parent"><a name="k1fopgv"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/skeedooshski" class="author submitter may-blank id-t2_2w3lp8yl" >skeedooshski</a><span class="userattrs">[<a class="submitter" title="submitter" href="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/">S</a>]</span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 16:24:03 2023 UTC" datetime="2023-09-20T16:24:03+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fopgvyrw"><input type="hidden" name="thing_id" value="t1_k1fopgv"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>That's an interesting one. I'll have a look as I'd be keen on adding the risky business RSS feed to something like that.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fopgv/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fopgv/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1firy2" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fopgv"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1g2003 noncollapsed   comment " id="thing_t1_k1g2003" onclick="click_thing(this)" data-fullname="t1_k1g2003" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="gamed0g" data-author-fullname="t2_f84b7" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1g2003/" ><p class="parent"><a name="k1g2003"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/gamed0g" class="author may-blank id-t2_f84b7" >gamed0g</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 17:41:17 2023 UTC" datetime="2023-09-20T17:41:17+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1g2003bvb"><input type="hidden" name="thing_id" value="t1_k1g2003"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>+1 for Feedly. It has loads of options to configure and refine your feeds</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1g2003/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1g2003/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1firy2" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1g2003"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1fiuv1 noncollapsed   comment " id="thing_t1_k1fiuv1" onclick="click_thing(this)" data-fullname="t1_k1fiuv1" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="HansGuntherboon" data-author-fullname="t2_mx14j" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fiuv1/" ><p class="parent"><a name="k1fiuv1"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/HansGuntherboon" class="author may-blank id-t2_mx14j" >HansGuntherboon</a><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Wed Sep 20 15:49:36 2023 UTC" datetime="2023-09-20T15:49:36+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fiuv16ta"><input type="hidden" name="thing_id" value="t1_k1fiuv1"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p><a href="https://infosec.exchange" rel="nofollow">https://infosec.exchange</a></p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fiuv1/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fiuv1/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fiuv1"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing noncollapsed   deleted controversial comment " onclick="click_thing(this)" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1etkxx/" ><p class="parent"></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><em>[deleted]</em> <time title="Wed Sep 20 13:08:13 2023 UTC" datetime="2023-09-20T13:08:13+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(3 children)</a></p><div class="usertext grayed"><input type="hidden" name="thing_id" value=""/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>[deleted]</p> +</div> +</div></div><ul class="flat-list buttons"></ul><div class="reportform report-t1_k1etkxx"></div></div><div class="child"><div id="siteTable_deleted" class="sitetable listing"><div class=" thing id-t1_k1f0w8k noncollapsed   comment " id="thing_t1_k1f0w8k" onclick="click_thing(this)" data-fullname="t1_k1f0w8k" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="moker" data-author-fullname="t2_31fvu" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f0w8k/" ><p class="parent"><a name="k1f0w8k"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/moker" class="author may-blank id-t2_31fvu" >moker</a><span class="userattrs"></span> <span class="score dislikes" title="7">7 points</span><span class="score unvoted" title="8">8 points</span><span class="score likes" title="9">9 points</span> <time title="Wed Sep 20 13:58:43 2023 UTC" datetime="2023-09-20T13:58:43+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f0w8kcn0"><input type="hidden" name="thing_id" value="t1_k1f0w8k"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Nah, that is not correct. You can join <a href="https://infosec.exchange">infosec.exchange</a> and follow anyone on <a href="https://infosec.exchange">infosec.exchange</a> or any of the other mastodon instances with that one account.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f0w8k/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f0w8k/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f0w8k"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1f3wf5 noncollapsed   comment " id="thing_t1_k1f3wf5" onclick="click_thing(this)" data-fullname="t1_k1f3wf5" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Gangrif" data-author-fullname="t2_13q213" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f3wf5/" ><p class="parent"><a name="k1f3wf5"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Gangrif" class="author may-blank id-t2_13q213" >Gangrif</a><span class="userattrs"></span> <span class="score dislikes" title="4">4 points</span><span class="score unvoted" title="5">5 points</span><span class="score likes" title="6">6 points</span> <time title="Wed Sep 20 14:18:22 2023 UTC" datetime="2023-09-20T14:18:22+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f3wf5u1j"><input type="hidden" name="thing_id" value="t1_k1f3wf5"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>no, you’re doing it wrong. you pick a home server based on your preferences. they all federate with eachother. i run my own and the experience has been awesome. you do need to give it time and start following folks to really get involved though.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f3wf5/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f3wf5/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f3wf5"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1f1tdt noncollapsed   comment " id="thing_t1_k1f1tdt" onclick="click_thing(this)" data-fullname="t1_k1f1tdt" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="bjh13" data-author-fullname="t2_3s36b" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f1tdt/" ><p class="parent"><a name="k1f1tdt"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/bjh13" class="author may-blank id-t2_3s36b" >bjh13</a><span class="userattrs"></span> <span class="score dislikes" title="9">9 points</span><span class="score unvoted" title="10">10 points</span><span class="score likes" title="11">11 points</span> <time title="Wed Sep 20 14:04:48 2023 UTC" datetime="2023-09-20T14:04:48+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f1tdtky4"><input type="hidden" name="thing_id" value="t1_k1f1tdt"/><div class="usertext-body may-blank-within md-container " ><div class="md"><blockquote> +<p>Oh, your people are on 8 different servers so you need 8 different accounts</p> +</blockquote> + +<p>The whole point of being federated is one account allows you to follow people on any of the other servers, so no you don't need 8 different accounts.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f1tdt/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f1tdt/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f1tdt"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1eji5w noncollapsed   controversial comment " id="thing_t1_k1eji5w" onclick="click_thing(this)" data-fullname="t1_k1eji5w" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="ThePorko" data-author-fullname="t2_1vnpf8ey" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eji5w/" ><p class="parent"><a name="k1eji5w"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/ThePorko" class="author may-blank id-t2_1vnpf8ey" >ThePorko</a><span class="userattrs"></span> <span class="score dislikes" title="-2">-2 points</span><span class="score unvoted" title="-1">-1 points</span><span class="score likes" title="0">0 points</span> <time title="Wed Sep 20 11:49:10 2023 UTC" datetime="2023-09-20T11:49:10+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1eji5w59w"><input type="hidden" name="thing_id" value="t1_k1eji5w"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I have never used twitter for that, too much garbage on there. I tend to do podcasts and youtube media.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eji5w/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eji5w/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1eji5w"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1e0j6k noncollapsed   controversial comment " id="thing_t1_k1e0j6k" onclick="click_thing(this)" data-fullname="t1_k1e0j6k" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1e0j6k/" ><p class="parent"><a name="k1e0j6k"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><span>[deleted]</span> <span class="score dislikes" title="-1">-1 points</span><span class="score unvoted" title="0">0 points</span><span class="score likes" title="1">1 point</span> <time title="Wed Sep 20 08:08:18 2023 UTC" datetime="2023-09-20T08:08:18+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1e0j6khpt"><input type="hidden" name="thing_id" value="t1_k1e0j6k"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>/g/</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1e0j6k/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1e0j6k/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1e0j6k"></div></div><div class="child"><div id="siteTable_t1_k1e0j6k" class="sitetable listing"><div class=" thing id-t1_k1e2rvb noncollapsed   comment " id="thing_t1_k1e2rvb" onclick="click_thing(this)" data-fullname="t1_k1e2rvb" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1e2rvb/" ><p class="parent"><a name="k1e2rvb"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><span>[deleted]</span> <span class="score dislikes" title="6">6 points</span><span class="score unvoted" title="7">7 points</span><span class="score likes" title="8">8 points</span> <time title="Wed Sep 20 08:37:53 2023 UTC" datetime="2023-09-20T08:37:53+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1e2rvbuyx"><input type="hidden" name="thing_id" value="t1_k1e2rvb"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Isn't the /sec/ general completely dead or at the very least only filled with "How to become 1337 haxxor"?</p> + +<p>It's been some time since I checked.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1e2rvb/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1e2rvb/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1e0j6k" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1e2rvb"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1etfwl noncollapsed   comment " id="thing_t1_k1etfwl" onclick="click_thing(this)" data-fullname="t1_k1etfwl" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="chicagoandy" data-author-fullname="t2_92ggj" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1etfwl/" ><p class="parent"><a name="k1etfwl"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/chicagoandy" class="author may-blank id-t2_92ggj" >chicagoandy</a><span class="userattrs"></span> <span class="score dislikes" title="-1">-1 points</span><span class="score unvoted" title="0">0 points</span><span class="score likes" title="1">1 point</span> <time title="Wed Sep 20 13:07:11 2023 UTC" datetime="2023-09-20T13:07:11+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1etfwlrm3"><input type="hidden" name="thing_id" value="t1_k1etfwl"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Um... Reddit.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1etfwl/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1etfwl/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1etfwl"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1f1b06 noncollapsed   comment " id="thing_t1_k1f1b06" onclick="click_thing(this)" data-fullname="t1_k1f1b06" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Chrishamilton2007" data-author-fullname="t2_5goj3" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f1b06/" ><p class="parent"><a name="k1f1b06"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Chrishamilton2007" class="author may-blank id-t2_5goj3" >Chrishamilton2007</a><span class="userattrs"></span> <span class="score dislikes" title="-1">-1 points</span><span class="score unvoted" title="0">0 points</span><span class="score likes" title="1">1 point</span> <time title="Wed Sep 20 14:01:25 2023 UTC" datetime="2023-09-20T14:01:25+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f1b06o27"><input type="hidden" name="thing_id" value="t1_k1f1b06"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>You can use reddit.</p> + +<p><a href="https://www.reddit.com/r/CASB+HackBloc+Malware+REMath+ReverseEngineering+blackhat+blueteamsec+computerforensics+crypto+netsec+netsecstudents+cyber+pwned+rootkit+vrd+xss+InfoSecInsiders/top/?sort=top&t=day" rel="nofollow">https://www.reddit.com/r/CASB+HackBloc+Malware+REMath+ReverseEngineering+blackhat+blueteamsec+computerforensics+crypto+netsec+netsecstudents+cyber+pwned+rootkit+vrd+xss+InfoSecInsiders/top/?sort=top&t=day</a></p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f1b06/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f1b06/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f1b06"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1h24vs noncollapsed   comment " id="thing_t1_k1h24vs" onclick="click_thing(this)" data-fullname="t1_k1h24vs" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Bllago" data-author-fullname="t2_5e3g42ih" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h24vs/" ><p class="parent"><a name="k1h24vs"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Bllago" class="author may-blank id-t2_5e3g42ih" >Bllago</a><span class="userattrs"></span> <span class="score dislikes" title="-1">-1 points</span><span class="score unvoted" title="0">0 points</span><span class="score likes" title="1">1 point</span> <time title="Wed Sep 20 21:06:31 2023 UTC" datetime="2023-09-20T21:06:31+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1h24vsydi"><input type="hidden" name="thing_id" value="t1_k1h24vs"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Twitter is full of CSAM. Everyone needs to leave it.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h24vs/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h24vs/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1h24vs"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1eqh9b collapsed collapsed-for-reason   comment " id="thing_t1_k1eqh9b" onclick="click_thing(this)" data-fullname="t1_k1eqh9b" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="KidBeene" data-author-fullname="t2_bfou7" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eqh9b/" ><p class="parent"><a name="k1eqh9b"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[+]</a><a href="https://www.reddit.com/user/KidBeene" class="author may-blank id-t2_bfou7" >KidBeene</a><span class="userattrs"></span> <span class="collapsed-reason">comment score below threshold</span><span class="score dislikes" title="-11">-11 points</span><span class="score unvoted" title="-10">-10 points</span><span class="score likes" title="-9">-9 points</span> <time title="Wed Sep 20 12:45:06 2023 UTC" datetime="2023-09-20T12:45:06+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(2 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1eqh9blt7"><input type="hidden" name="thing_id" value="t1_k1eqh9b"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>You were using Twitter for industry news? LOL Oh man... how much time do you have in the day?</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eqh9b/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eqh9b/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1eqh9b"></div></div><div class="child"><div id="siteTable_t1_k1eqh9b" class="sitetable listing"><div class=" thing id-t1_k1euxho noncollapsed   comment " id="thing_t1_k1euxho" onclick="click_thing(this)" data-fullname="t1_k1euxho" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="beagle_bathouse" data-author-fullname="t2_83rlaeff" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1euxho/" ><p class="parent"><a name="k1euxho"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/beagle_bathouse" class="author may-blank id-t2_83rlaeff" >beagle_bathouse</a><span class="userattrs"></span> <span class="score dislikes" title="3">3 points</span><span class="score unvoted" title="4">4 points</span><span class="score likes" title="5">5 points</span> <time title="Wed Sep 20 13:17:52 2023 UTC" datetime="2023-09-20T13:17:52+00:00" class="live-timestamp">5 months ago</time><time class="edited-timestamp" title="last edited 9 days ago" datetime="2024-02-09T18:57:46+00:00">*</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1euxhodgx"><input type="hidden" name="thing_id" value="t1_k1euxho"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>poor smell rhythm ink offend abounding person alive elderly dog</p> + +<p><em>This post was mass deleted and anonymized with <a href="https://redact.dev">Redact</a></em></p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1euxho/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1euxho/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1eqh9b" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1euxho"></div></div><div class="child"><div id="siteTable_t1_k1euxho" class="sitetable listing"><div class=" thing id-t1_k1gitjf noncollapsed   comment " id="thing_t1_k1gitjf" onclick="click_thing(this)" data-fullname="t1_k1gitjf" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="KidBeene" data-author-fullname="t2_bfou7" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gitjf/" ><p class="parent"><a name="k1gitjf"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/KidBeene" class="author may-blank id-t2_bfou7" >KidBeene</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 19:17:57 2023 UTC" datetime="2023-09-20T19:17:57+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1gitjfetc"><input type="hidden" name="thing_id" value="t1_k1gitjf"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Yeah, the thing that is wrong is using twitter for industry news.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gitjf/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gitjf/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1euxho" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1gitjf"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1eu1yr noncollapsed   comment " id="thing_t1_k1eu1yr" onclick="click_thing(this)" data-fullname="t1_k1eu1yr" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="TulkasDeTX" data-author-fullname="t2_8wfrrqxs" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eu1yr/" ><p class="parent"><a name="k1eu1yr"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/TulkasDeTX" class="author may-blank id-t2_8wfrrqxs" >TulkasDeTX</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 13:11:39 2023 UTC" datetime="2023-09-20T13:11:39+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1eu1yrkr4"><input type="hidden" name="thing_id" value="t1_k1eu1yr"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I still get good infosec content, but yeah I'm basically for the same thing, where to go when troll-land finally goes down</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eu1yr/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eu1yr/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1eu1yr"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1fbj2l noncollapsed   comment " id="thing_t1_k1fbj2l" onclick="click_thing(this)" data-fullname="t1_k1fbj2l" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="True2this" data-author-fullname="t2_efgrr" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fbj2l/" ><p class="parent"><a name="k1fbj2l"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/True2this" class="author may-blank id-t2_efgrr" >True2this</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 15:05:35 2023 UTC" datetime="2023-09-20T15:05:35+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fbj2l8uk"><input type="hidden" name="thing_id" value="t1_k1fbj2l"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Are you looking for just news feeds or something deeper? I use the open threat exchange from AlienVault. Good community - <a href="https://otx.alienvault.com" rel="nofollow">https://otx.alienvault.com</a></p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fbj2l/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fbj2l/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fbj2l"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1gzblu noncollapsed   comment " id="thing_t1_k1gzblu" onclick="click_thing(this)" data-fullname="t1_k1gzblu" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="manintheflask" data-author-fullname="t2_waj3n" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gzblu/" ><p class="parent"><a name="k1gzblu"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/manintheflask" class="author may-blank id-t2_waj3n" >manintheflask</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 20:50:49 2023 UTC" datetime="2023-09-20T20:50:49+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1gzblu0hg"><input type="hidden" name="thing_id" value="t1_k1gzblu"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I find this start[.]me URL pretty useful:<br/> +<a href="https://start.me/p/wMrA5z/cyber-threat-intelligence" rel="nofollow">https://start.me/p/wMrA5z/cyber-threat-intelligence</a></p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gzblu/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gzblu/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1gzblu"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1h1jio noncollapsed   comment " id="thing_t1_k1h1jio" onclick="click_thing(this)" data-fullname="t1_k1h1jio" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="netbroom" data-author-fullname="t2_gcsmap2" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h1jio/" ><p class="parent"><a name="k1h1jio"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/netbroom" class="author may-blank id-t2_gcsmap2" >netbroom</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 21:03:09 2023 UTC" datetime="2023-09-20T21:03:09+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1h1jios3b"><input type="hidden" name="thing_id" value="t1_k1h1jio"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Pulsedive has a free dashboard for infosec news</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h1jio/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h1jio/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1h1jio"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1j6zad noncollapsed   comment " id="thing_t1_k1j6zad" onclick="click_thing(this)" data-fullname="t1_k1j6zad" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="flusteredJonnies" data-author-fullname="t2_9ljkm" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1j6zad/" ><p class="parent"><a name="k1j6zad"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/flusteredJonnies" class="author may-blank id-t2_9ljkm" >flusteredJonnies</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Thu Sep 21 07:09:09 2023 UTC" datetime="2023-09-21T07:09:09+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1j6zad1ny"><input type="hidden" name="thing_id" value="t1_k1j6zad"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Dude I was getting the WEIRDEST content on X before I had to delete it because it was absurd. I only follow infosec people. Like half of my timeline became fight videos randomly. Like videos of people fighting liveleak style. Stuff that was so violent I surely thought would violate some policy, but had TONS of engagement.</p> + +<p>Not sure what they changed over there but no matter how often I scrolled past or reported or did behaviors to show the algo I was not interested in the content, it was all over my TL for like a month. Deleted the app as it just insisted on pushing me weird or violent content. Bummed because it was a great news source for a while.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1j6zad/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1j6zad/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1j6zad"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1j868n noncollapsed   comment " id="thing_t1_k1j868n" onclick="click_thing(this)" data-fullname="t1_k1j868n" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="VAsHachiRoku" data-author-fullname="t2_8dcuztg" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1j868n/" ><p class="parent"><a name="k1j868n"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/VAsHachiRoku" class="author may-blank id-t2_8dcuztg" >VAsHachiRoku</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Thu Sep 21 07:23:59 2023 UTC" datetime="2023-09-21T07:23:59+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1j868nr5h"><input type="hidden" name="thing_id" value="t1_k1j868n"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>We pay for threat intel company feeds like Mandiant, along with news and other information. Easier to have it come from a trusted source rather than many toxic places like X and Reddit. These both can draw in people with their own personal agendas and messages.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1j868n/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1j868n/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1j868n"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k27x83v noncollapsed   comment " id="thing_t1_k27x83v" onclick="click_thing(this)" data-fullname="t1_k27x83v" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Reshi-Snoo" data-author-fullname="t2_hj8duxb7" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k27x83v/" ><p class="parent"><a name="k27x83v"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Reshi-Snoo" class="author may-blank id-t2_hj8duxb7" >Reshi-Snoo</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Tue Sep 26 01:01:08 2023 UTC" datetime="2023-09-26T01:01:08+00:00" class="live-timestamp">4 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k27x83v9b2"><input type="hidden" name="thing_id" value="t1_k27x83v"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p><a href="https://vulnu.mattjay.com" rel="nofollow">Vulnerable U</a></p> + +<p>Unsupervised learning</p> + +<p>Tl;drsec </p> + +<p>Are my favorite newsletters.</p> +</div> +</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k27x83v/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k27x83v/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k27x83v"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div><script id="archived-popup" type="text/template"><div class="interstitial"><img class="interstitial-image" src="//www.redditstatic.com/interstitial-image-archived.png" alt="archived" height="150" width="150"><div class="interstitial-message md-container"><div class="md"><h3>This is an archived post. You won't be able to vote or comment.</h3><p>Posts are automatically archived after 6 months.</p></div></div><div class="buttons"><a href="/" class="c-btn c-btn-primary">Got It</a></div></div></script><script id="about-this-ad-popup" type="text/template"><h1 class="modal-title">About this ad</h1><div class="about-this-ad-body"></div><hr><div>Learn more about <a target="_blank" href="https://support.reddithelp.com/hc/en-us/articles/12731820767764-Control-the-ads-you-see-on-Reddit">controlling the ads you see on Reddit</a> or <a target="_blank" href="https://reddit.com/settings/privacy">manage your account settings.</a></div></script></div><script id="archived-popup" type="text/template"><div class="interstitial"><img class="interstitial-image" src="//www.redditstatic.com/interstitial-image-archived.png" alt="archived" height="150" width="150"><div class="interstitial-message md-container"><div class="md"><h3>This is an archived post. You won't be able to vote or comment.</h3><p>Posts are automatically archived after 6 months.</p></div></div><div class="buttons"><a href="/" class="c-btn c-btn-primary">Got It</a></div></div></script><script id="about-this-ad-popup" type="text/template"><h1 class="modal-title">About this ad</h1><div class="about-this-ad-body"></div><hr><div>Learn more about <a target="_blank" href="https://support.reddithelp.com/hc/en-us/articles/12731820767764-Control-the-ads-you-see-on-Reddit">controlling the ads you see on Reddit</a> or <a target="_blank" href="https://reddit.com/settings/privacy">manage your account settings.</a></div></script></div><div class="footer-parent"><div by-zero class="footer rounded"><div class="col"><ul class="flat-vert hover" ><li class="flat-vert title">about</li><li ><a href="https://redditblog.com" class="choice" >blog</a></li><li ><span class="separator"></span><a href="https://www.redditinc.com" class="choice" >about</a></li><li ><span class="separator"></span><a href="https://www.redditinc.com/advertising" class="choice" >advertising</a></li><li ><span class="separator"></span><a href="https://www.redditinc.com/careers" class="choice" >careers</a></li></ul></div><div class="col"><ul class="flat-vert hover" ><li class="flat-vert title">help</li><li ><a href="https://www.reddit.com/rules/" class="choice" >site rules</a></li><li ><span class="separator"></span><a href="https://www.reddithelp.com" class="choice" >Reddit help center</a></li><li ><span class="separator"></span><a href="https://www.reddit.com/wiki/reddiquette/" class="choice" >reddiquette</a></li><li ><span class="separator"></span><a href="https://www.reddit.com/help/healthycommunities/" class="choice" >mod guidelines</a></li><li ><span class="separator"></span><a href="https://www.reddit.com/contact/" class="choice" >contact us</a></li></ul></div><div class="col"><ul class="flat-vert hover" ><li class="flat-vert title">apps & tools</li><li ><a href="https://itunes.apple.com/us/app/reddit-the-official-app/id1064216828?mt=8" class="choice" >Reddit for iPhone</a></li><li ><span class="separator"></span><a href="https://play.google.com/store/apps/details?id=com.reddit.frontpage" class="choice" >Reddit for Android</a></li><li ><span class="separator"></span><a href="#" class="mweb-redirect-btn choice" >mobile website</a></li></ul></div><div class="col"><ul class="flat-vert hover" ><li class="flat-vert title"><3</li><li ><a href="https://www.reddit.com/premium/" class="buygold choice" >reddit premium</a></li></ul></div></div><p class="bottommenu">Use of this site constitutes acceptance of our <a href="https://www.reddit.com/help/useragreement" >User Agreement</a> and <a href="https://www.reddit.com/help/privacypolicy" >Privacy Policy</a>. © 2024 reddit inc. All rights reserved.</p><p class="bottommenu">REDDIT and the ALIEN Logo are registered trademarks of reddit inc.</p></div><script>var BETA_HOST = 'beta.reddit.com'; if (location.host === BETA_HOST) { r.config.https_endpoint = 'https://' + BETA_HOST; }</script><script id="login-popup" type="text/template"><!-- Login form function --><div id="desktop-onboarding-browse" class="c-step-sign-up"><div class="desktop-onboarding-step desktop-onboarding-step_sign-up"><div class="desktop-onboarding__col desktop-onboarding__col_sign-up_form"><div class="reddit-logo"><img width='200px' src="//www.redditstatic.com/logo.svg" /></div><h2 class="desktop-onboarding__title">Sign up to get your own personalized Reddit experience!</h2><p class="desktop-onboarding__description">By having a Reddit account, you can join, vote, and comment on all your favorite Reddit content. Sign up in just seconds.</p><div class="desktop-onboarding-sign-up__form-container c-is-create"><div class="desktop-onboarding-sign-up__form desktop-onboarding-sign-up__form_create"><h3 class="desktop-onboarding-sign-up__form-title">Enter email</h3><form class="sign-up-form" id="desktop-onboarding-sign-up-form" autocomplete="off"><div class="c-form-group "><label for="email" class="screenreader-only">email:</label><input name="email" id="desktop-onboarding-email" class="c-form-control" type="text" autofocus placeholder="email address" data-validate-url="/api/check_email.json" data-validate-on="keyup change blur" /><div class="c-form-control-feedback-wrapper "><span class="c-form-control-feedback c-form-control-feedback-throbber"></span><span class="c-form-control-feedback c-form-control-feedback-error" title=""></span><span class="c-form-control-feedback c-form-control-feedback-success"></span></div></div><button type="submit" class="c-btn c-btn-primary desktop-onboarding__next-button">Next</button><p class="desktop-onboarding-sign-up__form-note"><span>Already have an account?</span><a href="." class="desktop-onboarding-sign-up__form-toggler" data-form="login">Log In</a><a href="javascript: void 0;" class="skip-for-now">Skip for now</a></p></form></div><div class="desktop-onboarding-sign-up__form desktop-onboarding-sign-up__form_login"><h3 class="desktop-onboarding-sign-up__form-title">Log In</h3><form id="login-form" method="post" action="https://www.reddit.com/r/cybersecurity/post/login" class="form-v2 onboarding-login"><input type="hidden" name="op" value="login"><div class="c-form-group "><label for="user_login" class="screenreader-only">username</label><input value="" name="user" id="user_login" autofocus class="c-form-control" type="text" maxlength="20" tabindex="3" placeholder="username" ><div class="c-form-control-feedback-wrapper "><span class="c-form-control-feedback c-form-control-feedback-throbber"></span><span class="c-form-control-feedback c-form-control-feedback-error" title=""></span><span class="c-form-control-feedback c-form-control-feedback-success"></span></div></div><div class="c-form-group "><label for="passwd_login" class="screenreader-only">password</label><input id="passwd_login" class="c-form-control" name="passwd" type="password" tabindex="3" placeholder="password" ><div class="c-form-control-feedback-wrapper "><span class="c-form-control-feedback c-form-control-feedback-throbber"></span><span class="c-form-control-feedback c-form-control-feedback-error" title=""></span><span class="c-form-control-feedback c-form-control-feedback-success"></span></div></div><div class="desktop-onboarding-sign-up__form-note"><span>Don't have an account?</span><a href="." class="desktop-onboarding-sign-up__form-toggler" data-form="create">Sign up</a> |<a href="/password">Reset password</a></div><input type="hidden" value="yes" name="rem"/><div class="spacer"><div class="c-form-group g-recaptcha" data-sitekey="6LeTnxkTAAAAAN9QEuDZRpn90WwKk_R1TRW_g-JC"></div><span class="error BAD_CAPTCHA field-captcha" style="display:none"></span></div><div class="c-clearfix c-submit-group"><span class="c-form-throbber"></span><button type="submit" class="c-btn c-btn-primary c-pull-right" tabindex="3">log in</button></div><div><div class="c-alert c-alert-danger"></div><span class="status"></span></div></form></div></div><footer>By signing up, you agree to our <a href="https://www.reddit.com/help/useragreement/" >Terms</a> and that you have read our <a href="https://www.reddit.com/help/privacypolicy/" >Privacy Policy</a> and <a href="https://www.reddit.com/help/contentpolicy/" >Content Policy</a>.</footer></div><div class="desktop-onboarding__col desktop-onboarding__col_sign-up_image"></div></div><div class="desktop-onboarding-step desktop-onboarding-step_subreddit-picker"><div class="subreddit-picker-header"><h2 class="desktop-onboarding__title">Find the good stuff</h2><p class="desktop-onboarding__description">Reddit is filled with interest based communities, offering something for everyone. Check out some communities and we recommend you join at least 5.</p></div><div class="subreddit-picker"><ul class="subreddit-picker__categories"></ul><ul class="subreddit-picker__subreddits"></ul><div class="subreddit-picker__fail"><span>Something went wrong.</span><a href=".">Try Again?</a></div><div class="subreddit-picker__category-fail"><span>Something went wrong.</span><a href=".">Try Again?</a></div></div><footer><div class="subreddit-picker-progress"><div class="subreddit-picker-progress__track"><div class="subreddit-picker-progress__bar"></div></div><span class="subreddit-picker-progress__num">0</span><span>/</span><span class="subreddit-picker-progress__denom">5</span><span> <span class="subreddit-subscription-count">recommended communities</span></span></div><span class="desktop-onboarding__step-number">Step 2 of 3</span><div class="desktop-onboarding__buttons"><button class="c-btn desktop-onboarding__back-button">Back</button><button class="c-btn c-btn-primary desktop-onboarding__next-button">Next</button></div><div class="registration-error"></div></footer></div><div class="desktop-onboarding-step desktop-onboarding-step_username"><div class="desktop-onboarding__col desktop-onboarding__col_username_form"><h2 class="desktop-onboarding__title">Choose your username</h2><p class="desktop-onboarding__description">Your username is how other community members will see you. This name will be used to credit you for things you share on Reddit. What should we call you?</p><div class=desktop-onboarding-username-form><form id="register-form" method="post" action="https://www.reddit.com/r/cybersecurity/post/reg" autocomplete="off" class="form-v2 onboarding-login"><input type="hidden" name="op" value="reg"><input type="hidden" id="desktop-onboarding-register-email" name="email" value=""><input type="hidden" id="desktop-onboarding-subreddits" name="sr" value=""><div class="c-form-group "><label class="desktop-onboarding-sign-up__form-title" for="user_reg">Choose username</label><input value="" name="user" id="user_reg" autofocus class="c-form-control" type="text" maxlength="20" tabindex="2" placeholder="username" data-validate-url="/api/check_username.json" data-validate-min="3" autocomplete="new-username" ><div class="c-form-control-feedback-wrapper "><span class="c-form-control-feedback c-form-control-feedback-throbber"></span><span class="c-form-control-feedback c-form-control-feedback-error" title=""></span><span class="c-form-control-feedback c-form-control-feedback-success"></span></div></div><div class="c-form-group "><label for="passwd_reg" class="desktop-onboarding-sign-up__form-title">Set password</label><input id="passwd_reg" class="c-form-control" name="passwd" type="password" tabindex="2" placeholder="password" data-validate-url='/api/check_password.json' autocomplete='new-password'><div class="c-form-control-feedback-wrapper "><span class="c-form-control-feedback c-form-control-feedback-throbber"></span><span class="c-form-control-feedback c-form-control-feedback-error" title=""></span><span class="c-form-control-feedback c-form-control-feedback-success"></span></div></div><input type="hidden" name="passwd2" id="passwd2_reg" class="c-form-control"><input type="hidden" value="yes" name="rem"/><div class="spacer"><div class="c-form-group g-recaptcha" data-sitekey="6LeTnxkTAAAAAN9QEuDZRpn90WwKk_R1TRW_g-JC"></div><span class="error BAD_CAPTCHA field-captcha" style="display:none"></span></div><div><div class="c-alert c-alert-danger"></div><span class="status"></span><span class="error RATELIMIT field-ratelimit" style="display:none"></span><span class="error RATELIMIT field-vdelay" style="display:none"></span></div></form></div></div><div class="desktop-onboarding__col desktop-onboarding__col_username_picker"><div class="username-generator"><p class="desktop-onboarding__description">Having a hard time picking a name?<br />Here are some available suggestions.</p><div class="username-generator__suggestions"></div><a href="javascript: void 0;" class="username-generator__refresh-button">Refresh suggestions</a></div><footer><span class="desktop-onboarding__step-number">Step 3 of 3</span><div class="desktop-onboarding__buttons"><button class="c-btn desktop-onboarding__back-button">Back</button><button class="c-btn c-btn-primary desktop-onboarding__next-button">Submit</button></div></footer></div></div></div></script><script id="lang-popup" type="text/template"><form action="https://www.reddit.com/post/unlogged_options" method="post" id="pref-form" class="pretty-form short-text prefoptions"><input type="hidden" name="uh" value="" /><table class="content preftable"><tr><th>interface language</th><td class="prefright"><select id="lang" name="lang"><option selected='selected' value="en">English [en]</option><option value="af">Afrikaans [af] (*)</option><option value="ar">العربية [ar] (*)</option><option value="be">Беларуская мова [be] (*)</option><option value="bg">български език [bg]</option><option value="bn-IN">বাংলা [bn-IN] (*)</option><option value="bn-bd">বাংলা [bn-bd] (*)</option><option value="bs">Bosanski [bs] (*)</option><option value="ca">català [ca]</option><option value="cs">česky [cs]</option><option value="cy">Cymraeg [cy] (*)</option><option value="da">dansk [da]</option><option value="de">Deutsch [de]</option><option value="el">Ελληνικά [el]</option><option value="en-au">English (Australia) [en-au]</option><option value="en-ca">English (Canadian) [en-ca]</option><option value="en-gb">English (Great Britain) [en-gb]</option><option value="en-us">English [en-us]</option><option value="eo">Esperanto [eo] (*)</option><option value="es">español [es]</option><option value="es-ar">español [es-ar]</option><option value="es-cl">español [es-cl]</option><option value="es-mx">Español [es-mx]</option><option value="et">eesti keel [et] (*)</option><option value="eu">Euskara [eu]</option><option value="fa">فارسی [fa]</option><option value="fi">suomi [fi]</option><option value="fil">Filipino [fil] (*)</option><option value="fr">français [fr]</option><option value="fr-ca">Français [fr-ca]</option><option value="fy-NL">Frysk [fy-NL] (*)</option><option value="ga-ie">Gaeilge [ga-ie] (*)</option><option value="gd">Gàidhlig [gd]</option><option value="gl">Galego [gl] (*)</option><option value="he">עברית [he] (*)</option><option value="hi">मानक हिन्दी [hi] (*)</option><option value="hr">hrvatski [hr]</option><option value="hu">Magyar [hu]</option><option value="hy">Հայերեն լեզու [hy]</option><option value="id">Bahasa Indonesia [id] (*)</option><option value="is">íslenska [is]</option><option value="it">italiano (Italy) [it]</option><option value="ja">日本語 [ja]</option><option value="kn_IN">ಕನ್ನಡ [kn_IN]</option><option value="ko">한국어 [ko]</option><option value="la">Latin [la] (*)</option><option value="leet">1337 [leet]</option><option value="lol">LOL [lol]</option><option value="lt">lietuvių kalba [lt] (*)</option><option value="lv">latviešu valoda [lv]</option><option value="ms">Bahasa Melayu [ms] (*)</option><option value="mt-MT">Malti [mt-MT]</option><option value="nl">Nederlands [nl]</option><option value="nn">Nynorsk [nn]</option><option value="no">Norsk [no]</option><option value="pir">Arrrrrrrr! [pir] (*)</option><option value="pl">polski [pl]</option><option value="pt">português [pt] (*)</option><option value="pt-pt">português [pt-pt]</option><option value="pt_BR">português brasileiro [pt_BR]</option><option value="ro">română [ro]</option><option value="ru">русский [ru]</option><option value="sk">slovenčina [sk]</option><option value="sl">slovenščina [sl] (*)</option><option value="sr">српски језик [sr]</option><option value="sr-la">Srpski [sr-la]</option><option value="sv">Svenska [sv]</option><option value="ta">தமிழ் [ta]</option><option value="th">ภาษาไทย [th]</option><option value="tr">Türkçe [tr]</option><option value="uk">українська мова [uk]</option><option value="vi">Tiếng Việt [vi]</option><option value="zh">中文 [zh]</option><option value="zh-cn">简化字 [zh-cn]</option></select> <span class="details hover">(*) incomplete  <a href="https://www.reddit.com/r/i18n/wiki/getting_started">volunteer to translate</a></span></td></tr><tr><th>location</th><td class="preflight"><a href="https://www.reddit.com/settings/account/">set location preferences</a></td></tr><tr><td><input type="submit" class="btn save-preferences" value="save options"/></td></tr></table></form></script><img id="hsts_pixel" src="//reddit.com/static/pixel.png"><p class="debuginfo"><span class="icon">π</span> <span class="content">Rendered by PID 29 on  reddit-service-r2-slowlane-65c5c76ff5-v258h  at 2024-02-19 03:13:22.575220+00:00 running 5b0a0b2 country code: US.</span></p><script type="text/javascript" src="//www.redditstatic.com/reddit.en.lSSjgFdIksE.js"></script><script type="text/javascript" src="//www.redditstatic.com/spoiler-text.vsLMfxcst1g.js"></script><script type="text/javascript" src="//www.redditstatic.com/onetrust.6tPW2jUogoc.js"></script></body></html>
\ No newline at end of file diff --git a/test/fixtures/rich_media/yahoo.html b/test/fixtures/rich_media/yahoo.html new file mode 100644 index 000000000..41d8c5cd9 --- /dev/null +++ b/test/fixtures/rich_media/yahoo.html @@ -0,0 +1,12 @@ +<meta property="og:url" content="https://yahoo.com"> +<meta property="og:type" content="website"> +<meta property="og:title" content="Yahoo | Mail, Weather, Search, Politics, News, Finance, Sports & Videos"> +<meta property="og:description" content="Latest news coverage, email, free stock quotes, live scores and video are just the beginning. Discover more every day at Yahoo!"> +<meta property="og:image" content="https://s.yimg.com/cv/apiv2/social/images/yahoo_default_logo.png"> + +<meta name="twitter:card" content="summary_large_image"> +<meta property="twitter:domain" content="yahoo.com"> +<meta property="twitter:url" content="https://yahoo.com"> +<meta name="twitter:title" content="Yahoo | Mail, Weather, Search, Politics, News, Finance, Sports & Videos"> +<meta name="twitter:description" content="Latest news coverage, email, free stock quotes, live scores and video are just the beginning. Discover more every day at Yahoo!"> +<meta name="twitter:image" content="https://s.yimg.com/cv/apiv2/social/images/yahoo_default_logo.png"> diff --git a/test/fixtures/rsa_keys/key_1.pem b/test/fixtures/rsa_keys/key_1.pem new file mode 100644 index 000000000..3da357500 --- /dev/null +++ b/test/fixtures/rsa_keys/key_1.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA2gdPJM5bWarGZ6QujfQ296l1yEQohS5fdtnxYQc+RXuS1gqZ +R/jVGHG25o4tmwyCLClyREU1CBTOCQBsg+BSehXlxNR9fiB4KaVQW9MMNa2vhHuG +f7HLdILiC+SPPTV1Bi8LCpxJowiSpnFPP4BDDeRKib7nOxll9Ln9gEpUueKKabsQ +EQKCmEJYhIz/8g5R0Qz+6VjASdejDjTEdZbr/rwyldRRjIklyeZ3lBzB/c8/51wn +HT2Dt0r9NiapxYC3oNhbE2A+4FU9pZTqS8yc3KqWZAy74snaRO9QQSednKlOJpXP +V3vwWo5CxuSNLttV7zRcrqeYOkIVNF4dQ/bHzQIDAQABAoIBADTCfglnEj4BkF92 +IHnjdgW6cTEUJUYNMba+CKY1LYF85Mx85hi/gzmWEu95yllxznJHWUpiAPJCrpUJ +EDldaDf44pAd53xE+S8CvQ5rZNH8hLOnfKWb7aL1JSRBm9PxAq+LZL2dkkgsg+hZ +FRdFv3Q2IT9x/dyUSdLNyyVnV1dfoya/7zOFc7+TwqlofznzrlBgNoAe8Lb4AN/q +itormPxskqATiq11XtP4F6eQ556eRgHCBxmktx/rRDl6f9G9dvjRQOA2qZlHQdFq +kjOZsrvItL46LdVoLPOdCYG+3HFeKoDUR1NNXEkt66eqmEhLY4MgzGUT1wqXWk7N +XowZc9UCgYEA+L5h4PhANiY5Kd+PkRI8zTlJMv8hFqLK17Q0p9eL+mAyOgXjH9so +QutJf4wU+h6ESDxH+1tCjCN307uUqT7YnT2zHf3b6GcmA+t6ewxfxOY2nJ82HENq +hK1aodnPTvRRRqCGfrx9qUHRTarTzi+2u86zH+KoMHSiuzn4VpQhg4MCgYEA4GOL +1tLR9+hyfYuMFo2CtQjp3KpJeGNKEqc33vFD05xJQX+m5THamBv8vzdVlVrMh/7j +iV85mlA7HaaP+r5DGwtonw9bqY76lYRgJJprsS5lHcRnXsDmU4Ne8RdB3dHNsT5P +n4P6v8y4jaT638iJ/qLt4e8itOBlZwS//VIglm8CgYEA7KXD3RKRlHK9A7drkOs2 +6VBM8bWEN1LdhGYvilcpFyUZ49XiBVatcS0EGdKdym/qDgc7vElQgJ7ly4y0nGfs +EXy3whrYcrxfkG8hcZuOKXeUEWHvSuhgmKWMilr8PfN2t6jVDBIrwzGY/Tk+lPUT +9o1qITW0KZVtlI5MU6JOWB0CgYAHwwnETZibxbuoIhqfcRezYXKNgop2EqEuUgB5 +wsjA2igijuLcDMRt/JHan3RjbTekAKooR1X7w4i39toGJ2y008kzr1lRXTPH1kNp +ILpW767pv7B/s5aEDwhKuK47mRVPa0Nf1jXnSpKbu7g943b6ivJFnXsK3LRFQwHN +JnkgGwKBgGUleQVd2GPr1dkqLVOF/s2aNB/+h2b1WFWwq0YTnW81OLwAcUVE4p58 +3GQgz8PCsWbNdTb9yFY5fq0fXgi0+T54FEoZWH09DrOepA433llAwI6sq7egrFdr +kKQttZMzs6ST9q/IOF4wgqSnBjjTC06vKSkNAlXJz+LMvIRMeBr0 +-----END RSA PRIVATE KEY----- diff --git a/test/fixtures/rsa_keys/key_2.pem b/test/fixtures/rsa_keys/key_2.pem new file mode 100644 index 000000000..7a8e8e670 --- /dev/null +++ b/test/fixtures/rsa_keys/key_2.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAwu0VqVGRVDW09V3zZ0+08K9HMKivIzIInO0xim3jbfVcg8r1 +sR7vNLorYAB6TDDlXYAWKx1OxUMZusbOigrpQd+5wy8VdCogDD7qk4bbZ+NjXkuD +ETzrQsGWUXe+IdeH8L0Zh0bGjbarCuA0qAeY1TEteGl+Qwo2dsrBUH7yKmWO6Mz9 +XfPshrIDOGo4QNyVfEBNGq2K9eRrQUHeAPcM2/qu4ZAZRK+VCifDZrF8ZNpoAsnS +R2mJDhOBUMvI/ZaxOc2ry4EzwcS4uBaM2wONkGWDaqO6jNAQflaX7vtzOAeJB7Dt +VKXUUcZAGN7uI3c2mG5IKGMhTYUtUdrzmqmtZwIDAQABAoIBAQCHBJfTf3dt4AGn +T9twfSp06MQj9UPS2i5THI0LONCm8qSReX0zoZzJZgbzaYFM0zWczUMNvDA6vR7O +XDTmM2acxW4zv6JZo3Ata0sqwuepDz1eLGnt/8dppxQK/ClL4bH8088h/6k6sgPJ +9cEjfpejXHwFgvT9VM6i/BBpRHVTXWuJqwpDtg+bleQNN3L3RapluDd7BGiKoCwQ +cCTKd+lxTu9gVJkbRTI/Jn3kV+rnedYxHTxVp5cU1qIabsJWBcdDz25mRHupxQsn +JbQR4+ZnRLeAsC6WJZtEJz2KjXgBaYroHbGZY3KcGW95ILqiCJoJJugbW1eABKnN +Q5k8XVspAoGBAPzGJBZuX3c0quorhMIpREmGq2vS6VCQwLhH5qayYYH1LiPDfpdq +69lOROxZodzLxBgTf5z/a5kBF+eNKvOqfZJeRTxmllxxO1MuJQuRLi/b7BHHLuyN +Eea+YwtehA0T0CbD2hydefARNDruor2BLvt/kt6qEoIFiPauTsMfXP39AoGBAMVp +8argtnB+vsk5Z7rpQ4b9gF5QxfNbA0Hpg5wUUdYrUjFr50KWt1iowj6AOVp/EYgr +xRfvOQdYODDH7R5cjgMbwvtpHo39Zwq7ewaiT1sJXnpGmCDVh+pdTHePC5OOXnxN +0USK3M4KjltjVqJo7xPPElgJvCejudD47mtHMaQzAoGBAIFQ/PVc0goyL55NVUXf +xse21cv7wtEsvOuKHT361FegD1LMmN7uHGq32BryYBSNSmzmzMqNAYbtQEV9uxOd +jVBsWg9kjFgOtcMAQIOCapahdExEEoWCRj49+H3AhN4L3Nl4KQWqqs9efdIIc8lv +ZZHU2lZ/u6g5HLDWzASW7wQhAoGAdERPRrqN+HdNWinrA9Q6JxjKL8IWs5rYsksb +biMxh5eAEwdf7oHhfd/2duUB4mCQLMjKjawgxEia33AAIS+VnBMPpQ5mJm4l79Y3 +QNL7Nbyw3gcRtdTM9aT5Ujj3MnJZB5C1PU8jeF4TNZOuBH0UwW/ld+BT5myxFXhm +wtvtSq0CgYEA19b0/7il4Em6uiLOmYUuqaUoFhUPqzjaS6OM/lRAw12coWv/8/1P +cwaNZHNMW9Me/bNH3zcOTz0lxnYp2BeRehjFYVPRuS1GU7uwqKtlL2wCPptTfAhN +aJWIplzUCTg786u+sdNZ0umWRuCLoUpsKTgP/yt4RglzEcfxAuBDljk= +-----END RSA PRIVATE KEY----- diff --git a/test/fixtures/rsa_keys/key_3.pem b/test/fixtures/rsa_keys/key_3.pem new file mode 100644 index 000000000..fbd25c80f --- /dev/null +++ b/test/fixtures/rsa_keys/key_3.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA0GvzqZ3r78GLa7guGn+palKRLGru4D4jnriHgfrUAJrdLyZ5 +9d0zAA4qnS2L6YAMoPPBhBUtIV5e2sn1+rwTClWU3dm3FyBAeqdeIBKN+04AyrUc +HXYaZtOPJXCTeytzoSQE359Tq6+xwgoHlUWSWxQF51/z/PDQcUvqFjJqAtdiDchd +3CiFRtdjegyxXGnqvPmBix+vEjDytcVydfch+R1Twf6f5EL7a1jFVWNGcratYBEl +nqOWKI2fBu/WA8QlrcVW5zmtZo9aJ6IrFddQgQTxPk/mEHgCzv8tbCRI9TxiXeYH +YqxZFYBW40xbZQwGRjaYHJlIRYp9+TOynW9OZQIDAQABAoIBAQC97cIMDbdVsyAk +N6D70N5H35ofygqJGtdG6o3B6xuKuZVaREvbu4mgQUigF0Nqs5/OhJMSlGGeCOuT +oXug1Abd4gNY7++jCWb43tAtlfsAyaJ7FvPZ/SguEBhgW+hp07z5WWN/jSeoSuFI +G++xHcczbFm88XncRG8O78kQFTz5/DlQYkFXfbqpuS3BqxnrACpDCUfrUwZNYFIp +CUNq21jdifhHwlS0K3PX8A5HdOYeVnVHaE78LGE4oJVHwcokELv+PYqarWZq/a6L +vKU3yn2+4pj2WO490iGQaRKVM35vrtjdVxiWEIUiFc3Jg5fKZA3wuHXoF1N1DpPO +BO6Att55AoGBAP/nC2szmDcnU5Sh8LDeQbL+FpSBwOmFnmel5uqbjKnDzf9emPQu +NFUls1N9OGgyUq08TnmcY/7wLZzcu7Y9XOUURuYtx9nGRs4RmE2VEBhK1r7CkDIx +oOb+NtdqnPtQASAxCHszoGCFxpuV7UVoo2SRgc+M4ceX128arvBUtvdrAoGBANCA +RuO3eelkXaJoCeogEUVWXZ6QmPeYzbMD4vg2DM0ynUbReyuEIIhn+SR7tehlj5ie +4T3ixVdur6k+YUdiFhUYgXaHBJWHoHl1lrU3ZON8n7AeEk9ft6gg4L07ouj78UMZ +sArJIlU5mLnW02zbV9XryU39dIgpQREqC0bIOtVvAoGBAORv1JKq6Rt7ALJy6VCJ +5y4ogfGp7pLHk8NEpuERYDz/rLllMbbwNAk6cV17L8pb+c/pQMhwohcnQiCALxUc +q/tW4X+CqJ+vzu8PZ90Bzu9Qh2iceGpGQTNTBZPA+UeigI7DFqYcTPM9GDE1YiyO +nyUcezvSsI4i7s6gjD+/7+DnAoGABm3+QaV1z/m1XX3B2IN2pOG971bcML54kW2s +QSVBjc5ixT1OhBAGBM7YAwUBnhILtJQptAPbPBAAwMJYs5/VuH7R9zrArG/LRhOX +Oy1jIhTEw+SZgfMcscWZyJwfMPob/Yq8QAjl0yT8jbaPPIsjEUi9I3eOcWh8RjA6 +ussP7WcCgYEAm3yvJR9z6QGoQQwtDbwjyZPYOSgK9wFS/65aupi6cm/Qk2N1YaLY +q2amNrzNsIc9vQwYGEHUwogn4MieHk96V7m2f0Hx9EHCMwizU9EiS6oyiLVowTG6 +YsBgSzcpnt0Vkgil4CQks5uQoan0tubEUQ5DI79lLnb02n4o46iAYK0= +-----END RSA PRIVATE KEY----- diff --git a/test/fixtures/rsa_keys/key_4.pem b/test/fixtures/rsa_keys/key_4.pem new file mode 100644 index 000000000..f72b29fb1 --- /dev/null +++ b/test/fixtures/rsa_keys/key_4.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAw6MLRbP/henX2JxwdMkQlskKghBoMyUPu9kZpUQ9yYfIm9I4 +a3gEfzef75jKLOSf+BkZulvEUGjC+VnkpV3s+OZCSq81Ykv5PHuTqbj8Cn/dEt/g +lBXxPcOBKWqa+1cDX6QVIVJsBihLB/1b64H3U96Yu9+knmXvT1Az5MFA2KtSq7HJ +O+GJNn0EMI7xwPz/atUGlMLrhzwS4UDpw9CAaRPojplJYl4K1JMCFTgTt3hJILXZ +tw1MKTeeyWzNiuQRBQJuCnqfvsBYsasIlHWfqIL/uBzcGHHCIK5ZW9luntJXyLVj +zzaF7etIJk1uddM2wnqOOaVyqbssZXGt7Tb9IQIDAQABAoIBAH5QJRUKFK8Xvp9C +0nD06NsSTtCPW1e6VCBLGf3Uw7f9DY9d+cOZp/2jooYGNnMp4gdD3ZKvcV8hZNGu +Mqx6qmhB8wdZfLRMrU1Z1Is+vqzgxZJMLiouyKXCNwDQreQd2DXGMUZkew62sUsl +UFYMge4KyL50tUr4Mb0Z4YePJxk804tcqgw0n+D0lR7ZKhSqoQpoMqEiO+27Yw7E +Txj/MKH8f/ZJ6LBLRISOdBOrxonHqqeYWchczykCwojOZc3bIlWZGhg727dFTHDC +yrj3/zsZ2hy+TQsucCFY0RljIbacmHvrF/VqfhTIhg98H0F27V/jiPGsdKhptyst +E9iQVMkCgYEA42ge4H2Wl42sRh61GOrOgzzr0WZS54bF5skMxiGGnLwnb82rwUBt +xw94PRORJbV9l+2fkxbfiW0uzornfN8OBHSB64Pcjzzbl5Qm+eaDOiuTLtakYOWQ +/ipGqw8iE4J9iRteZCo8GnMxWbTkYCporTlFDTeYguXmwR4yCXtlCbMCgYEA3DxM +7R5HMUWRe64ucdekMh742McS8q/X5jdN9iFGy0M8P1WTyspSlaPDXgjaO4XqpRqg +djkL993kCDvOAiDl6Tpdiu1iFcOaRLb19Tj1pm8sKdk6X4d10U9lFri4NVYCmvVi +yOahUYFK/k5bA+1o+KU9Pi82H36H3WNeF4evC9sCgYEAs1zNdc04uQKiTZAs0KFr +DzI+4aOuYjT35ObQr3mD/h2dkV6MSNmzfF1kPfAv/KkgjXN7+H0DBRbb40bF/MTF +/peSXZtcnJGote7Bqzu4Z2o1Ja1ga5jF+uKHaKZ//xleQIUYtzJkw4v18cZulrb8 +ZxyTrTAbl6sTjWBuoPH1qGcCgYEAsQNahR9X81dKJpGKTQAYvhw8wOfI5/zD2ArN +g62dXBRPYUxkPJM/q3xzs6oD1eG+BjQPktYpM3FKLf/7haRxhnLd6qL/uiR8Ywx3 +RkEg2EP0yDIMA+o5nSFmS8vuaxgVgf0HCBiuwnbcEuhhqRdxzp/pSIjjxI6LnzqV +zu3EmQ8CgYEAhq8Uhvw+79tK7q2PCjDbiucA0n/4a3aguuvRoEh7F93Pf6VGZmT+ +Yld54Cd4P5ATI3r5YdD+JBuvgNMOTVPCaD/WpjbJKnrpNEXtXRQD6LzAXZDNk0sF +IO9i4gjhBolRykWn10khoPdxw/34FWBP5SxU1JYk75NQXvI3TD+5xbU= +-----END RSA PRIVATE KEY----- diff --git a/test/fixtures/rsa_keys/key_5.pem b/test/fixtures/rsa_keys/key_5.pem new file mode 100644 index 000000000..49342b54e --- /dev/null +++ b/test/fixtures/rsa_keys/key_5.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpgIBAAKCAQEA0jdKtMkgqnEGO3dn4OKxtggfFDzv+ddXToO0cdPXkUgPajCo +UGPunz+A1KmkAmLY0Vwk0tkOmKK8GFHek/5zQ+1N2FHBi19fbwlJk7hzh5OiYRhu +YZi0d6LsqEMKhDk6NqIeiFmOe2YHgklVvZV0hebvHlHLgzDhYrDltSPe33UZa3MS +g2Knf4WQAjLOo2BAb+oyj/UNXeAqaMGcOr6/kAHPcODW2EGhF3H3umFLv7t/Kq5i +WPBgarbCGPR5qq9SW5ZIjS3Sz0dl105Grw8wU23CC/2IBZ5vNiu+bkmLEoh/KpX2 +YBILoLmwtVX0Qxc15CrpOi12p+/4pLR8kuEowQIDAQABAoIBAQDMDQ3AJMdHisSQ +7pvvyDzWRFXesDQE4YmG1gNOxmImTLthyW9n8UjMXbjxNOXVxxtNRdMcs8MeWECa +nsWeBEzgr7VzeBCV9/LL9kjsUgwamyzwcOWcaL0ssAJmZgUMSfx+0akvkzbiAyzg +w8ytZSihXYPYe28/ni/5O1sOFI6feenOnJ9NSmVUA24c9TTJGNQs7XRUMZ8f9wt6 +KwRmYeNDKyqH7NvLmmKoDp6m7bMDQxWArVTAoRWTVApnj35iLQtmSi8DBdw6xSzQ +fKpUe/B4iQmMNxUW7KmolOvCIS5wcYZJE+/j7xshA2GGnOpx4aC+N+w2GSX4Bz/q +OnYSpGUBAoGBAOwnSeg17xlZqmd86qdiCxg0hRtAjwrd7btYq6nkK+t9woXgcV99 +FBS3nLbk/SIdXCW8vHFJTmld60j2q2kdestYBdHznwNZJ4Ee8JhamzcC64wY7O0x +RameO/6uoKS4C3VF+Zc9CCPfZOqYujkGvSqbTjFZWuFtDp0GHDk+qEIRAoGBAOPh ++PCB2QkGgiujSPmuCT5PTuNylAug3D4ZdMRKpQb9Rnzlia1Rpdrihq+PvB2vwa+S +mB6dgb0E7M2AyEMVu5buris0mVpRdmEeLCXR8mYJ48kOslIGArEStXDetfbRaXdK +7vf4APq2d78AQYldU2fYlo754Dh/3MZIguzpqMuxAoGBAIDJqG/AQiYkFV+c62ff +e0d3FQRYv+ngQE9Eu1HKwv0Jt7VFQu8din8F56yC013wfxmBhY+Ot/mUo8VF6RNJ +ZXdSCNKINzcfPwEW+4VLHIzyxbzAty1gCqrHRdbOK4PJb05EnCqTuUW/Bg0+v4hs +GWwMCKe3IG4CCM8vzuKVPjPRAoGBANYCQtJDb3q9ZQPsTb1FxyKAQprx4Lzm7c9Y +AsPRQhhFRaxHuLtPQU5FjK1VdBoBFAl5x2iBDPVhqa348pml0E0Xi/PBav9aH61n +M5i1CUrwoL4SEj9bq61133XHgeXwlnZUpgW0H99T+zMh32pMfea5jfNqETueQMzq +DiLF8SKRAoGBAOFlU0kRZmAx3Y4rhygp1ydPBt5+zfDaGINRWEN7QWjhX2QQan3C +SnXZlP3POXLessKxdCpBDq/RqVQhLea6KJMfP3F0YbohfWHt96WjiriJ0d0ZYVhu +34aUM2UGGG0Kia9OVvftESBaXk02vrY9zU3LAVAv0eLgIADm1kpj85v7 +-----END RSA PRIVATE KEY----- diff --git a/test/fixtures/tesla_mock/aimu@misskey.io.json b/test/fixtures/tesla_mock/aimu@misskey.io.json new file mode 100644 index 000000000..9ff4cb6d0 --- /dev/null +++ b/test/fixtures/tesla_mock/aimu@misskey.io.json @@ -0,0 +1,64 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "Hashtag": "as:Hashtag", + "quoteUrl": "as:quoteUrl", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "featured": "toot:featured", + "discoverable": "toot:discoverable", + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "misskey": "https://misskey.io/ns#", + "_misskey_content": "misskey:_misskey_content", + "_misskey_quote": "misskey:_misskey_quote", + "_misskey_reaction": "misskey:_misskey_reaction", + "_misskey_votes": "misskey:_misskey_votes", + "_misskey_talk": "misskey:_misskey_talk", + "isCat": "misskey:isCat", + "vcard": "http://www.w3.org/2006/vcard/ns#" + } + ], + "type": "Person", + "id": "https://misskey.io/users/83ssedkv53", + "inbox": "https://misskey.io/users/83ssedkv53/inbox", + "outbox": "https://misskey.io/users/83ssedkv53/outbox", + "followers": "https://misskey.io/users/83ssedkv53/followers", + "following": "https://misskey.io/users/83ssedkv53/following", + "sharedInbox": "https://misskey.io/inbox", + "endpoints": { + "sharedInbox": "https://misskey.io/inbox" + }, + "url": "https://misskey.io/@aimu", + "preferredUsername": "aimu", + "name": "あいむ", + "summary": "<p><span>わずかな作曲要素 巣穴で独り言<br>Twitter </span><a href=\"https://twitter.com/aimu_53\">https://twitter.com/aimu_53</a><span><br>Soundcloud </span><a href=\"https://soundcloud.com/aimu-53\">https://soundcloud.com/aimu-53</a></p>", + "icon": { + "type": "Image", + "url": "https://s3.arkjp.net/misskey/webpublic-3f7e93c0-34f5-443c-acc0-f415cb2342b4.jpg", + "sensitive": false, + "name": null + }, + "image": { + "type": "Image", + "url": "https://s3.arkjp.net/misskey/webpublic-2db63d1d-490b-488b-ab62-c93c285f26b6.png", + "sensitive": false, + "name": null + }, + "tag": [], + "manuallyApprovesFollowers": false, + "discoverable": true, + "publicKey": { + "id": "https://misskey.io/users/83ssedkv53#main-key", + "type": "Key", + "owner": "https://misskey.io/users/83ssedkv53", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1ylhePJ6qGHmwHSBP17b\nIosxGaiFKvgDBgZdm8vzvKeRSqJV9uLHfZL3pO/Zt02EwaZd2GohZAtBZEF8DbMA\n3s93WAesvyGF9mjGrYYKlhp/glwyrrrbf+RdD0DLtyDwRRlrxp3pS2lLmv5Tp1Zl\npH+UKpOnNrpQqjHI5P+lEc9bnflzbRrX+UiyLNsVAP80v4wt7SZfT/telrU6mDru\n998UdfhUo7bDKeDsHG1PfLpyhhtfdoZub4kBpkyacHiwAd+CdCjR54Eu7FDwVK3p\nY3JcrT2q5stgMqN1m4QgSL4XAADIotWwDYttTJejM1n9dr+6VWv5bs0F2Q/6gxOp\nu5DQZLk4Q+64U4LWNox6jCMOq3fYe0g7QalJIHnanYQQo+XjoH6S1Aw64gQ3Ip2Y\nZBmZREAOR7GMFVDPFnVnsbCHnIAv16TdgtLgQBAihkWEUuPqITLi8PMu6kMr3uyq\nYkObEfH0TNTcqaiVpoXv791GZLEUV5ROl0FSUANLNkHZZv29xZ5JDOBOR1rNBLyH\ngVtW8rpszYqOXwzX23hh4WsVXfB7YgNvIijwjiaWbzsecleaENGEnLNMiVKVumTj\nmtyTeFJpH0+OaSrUYpemRRJizmqIjklKsNwUEwUb2WcUUg92o56T2obrBkooabZe\nwgSXSKTOcjsR/ju7+AuIyvkCAwEAAQ==\n-----END PUBLIC KEY-----\n" + }, + "isCat": true, + "vcard:bday": "5353-05-03" +} diff --git a/test/fixtures/tesla_mock/framatube.org_host_meta b/test/fixtures/tesla_mock/framatube.org_host_meta index 91516ff6d..02e25bd64 100644 --- a/test/fixtures/tesla_mock/framatube.org_host_meta +++ b/test/fixtures/tesla_mock/framatube.org_host_meta @@ -1,2 +1,2 @@ <?xml version="1.0" encoding="UTF-8"?> -<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><hm:Host xmlns:hm="http://host-meta.net/xrd/1.0">framatube.org</hm:Host><Link rel="lrdd" template="http://framatube.org/main/xrd?uri={uri}"><Title>Resource Descriptor</Title></Link></XRD> +<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><hm:Host xmlns:hm="http://host-meta.net/xrd/1.0">framatube.org</hm:Host><Link rel="lrdd" template="https://framatube.org/main/xrd?uri={uri}"><Title>Resource Descriptor</Title></Link></XRD> diff --git a/test/fixtures/tesla_mock/gleasonator.com_host_meta b/test/fixtures/tesla_mock/gleasonator.com_host_meta new file mode 100644 index 000000000..c1a432519 --- /dev/null +++ b/test/fixtures/tesla_mock/gleasonator.com_host_meta @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"> + <Link rel="lrdd" template="https://gleasonator.com/.well-known/webfinger?resource={uri}" type="application/xrd+xml" /> +</XRD>
\ No newline at end of file diff --git a/test/fixtures/tesla_mock/helene@p.helene.moe.json b/test/fixtures/tesla_mock/helene@p.helene.moe.json new file mode 100644 index 000000000..d7444817f --- /dev/null +++ b/test/fixtures/tesla_mock/helene@p.helene.moe.json @@ -0,0 +1,50 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://p.helene.moe/schemas/litepub-0.1.jsonld", + { + "@language": "und" + } + ], + "alsoKnownAs": [], + "attachment": [ + { + "name": "Timezone", + "type": "PropertyValue", + "value": "UTC+2 (Paris/Berlin)" + } + ], + "capabilities": { + "acceptsChatMessages": true + }, + "discoverable": true, + "endpoints": { + "oauthAuthorizationEndpoint": "https://p.helene.moe/oauth/authorize", + "oauthRegistrationEndpoint": "https://p.helene.moe/api/v1/apps", + "oauthTokenEndpoint": "https://p.helene.moe/oauth/token", + "sharedInbox": "https://p.helene.moe/inbox", + "uploadMedia": "https://p.helene.moe/api/ap/upload_media" + }, + "featured": "https://p.helene.moe/users/helene/collections/featured", + "followers": "https://p.helene.moe/users/helene/followers", + "following": "https://p.helene.moe/users/helene/following", + "icon": { + "type": "Image", + "url": "https://p.helene.moe/media/9a39209daa5a66b7ebb0547b08bf8360aa9d8d65a4ffba2603c6ffbe6aecb432.jpg" + }, + "id": "https://p.helene.moe/users/helene", + "inbox": "https://p.helene.moe/users/helene/inbox", + "manuallyApprovesFollowers": false, + "name": "Hélène", + "outbox": "https://p.helene.moe/users/helene/outbox", + "preferredUsername": "helene", + "publicKey": { + "id": "https://p.helene.moe/users/helene#main-key", + "owner": "https://p.helene.moe/users/helene", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtoSBPU/VS2Kx3f6ap3zv\nZVacJsgUfaoFb3c2ii/FRh9RmRVlarq8sJXcjsQt1e0oxWaWJaIDDwyKZPt6hXae\nrY/AiGGeNu+NA+BtY7l7+9Yu67HUyT62+1qAwYHKBXX3fLOPs/YmQI0Tt0c4wKAG\nKEkiYsRizghgpzUC6jqdKV71DJkUZ8yhckCGb2fLko1ajbWEssdaP51aLsyRMyC2\nuzeWrxtD4O/HG0ea4S6y5X6hnsAHIK4Y3nnyIQ6pn4tOsl3HgqkjXE9MmZSvMCFx\nBq89TfZrVXNa2gSZdZLdbbJstzEScQWNt1p6tA6rM+e4JXYGr+rMdF3G+jV7afI2\nFQIDAQAB\n-----END PUBLIC KEY-----\n\n" + }, + "summary": "I can speak: Français, English, Deutsch (nicht sehr gut), 日本語 (not very well)", + "tag": [], + "type": "Person", + "url": "https://p.helene.moe/users/helene" +}
\ No newline at end of file diff --git a/test/fixtures/tesla_mock/https___lm.kazv.moe_users_mewmew.xml b/test/fixtures/tesla_mock/https___lm.kazv.moe_users_mewmew.xml new file mode 100644 index 000000000..b9e8dbbf5 --- /dev/null +++ b/test/fixtures/tesla_mock/https___lm.kazv.moe_users_mewmew.xml @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Subject>acct:mewmew@lm.kazv.moe</Subject><Alias>https://lm.kazv.moe/users/mewmew</Alias><Alias>https://lm.kazv.moe/users/tester</Alias><Alias>https://lm.kazv.moe/users/testuser</Alias><Link href="https://lm.kazv.moe/users/mewmew" rel="http://webfinger.net/rel/profile-page" type="text/html" /><Link href="https://lm.kazv.moe/users/mewmew" rel="self" type="application/activity+json" /><Link href="https://lm.kazv.moe/users/mewmew" rel="self" type="application/ld+json; profile="https://www.w3.org/ns/activitystreams"" /><Link rel="http://ostatus.org/schema/1.0/subscribe" template="https://lm.kazv.moe/ostatus_subscribe?acct={uri}" /></XRD> diff --git a/test/fixtures/tesla_mock/lm.kazv.moe_host_meta b/test/fixtures/tesla_mock/lm.kazv.moe_host_meta new file mode 100644 index 000000000..02e6f055e --- /dev/null +++ b/test/fixtures/tesla_mock/lm.kazv.moe_host_meta @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" template="https://lm.kazv.moe/.well-known/webfinger?resource={uri}" type="application/xrd+xml" /></XRD> diff --git a/test/fixtures/tesla_mock/mametsuko@mk.absturztau.be.json b/test/fixtures/tesla_mock/mametsuko@mk.absturztau.be.json new file mode 100644 index 000000000..d8c13f775 --- /dev/null +++ b/test/fixtures/tesla_mock/mametsuko@mk.absturztau.be.json @@ -0,0 +1,65 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "Hashtag": "as:Hashtag", + "quoteUrl": "as:quoteUrl", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "featured": "toot:featured", + "discoverable": "toot:discoverable", + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "misskey": "https://misskey-hub.net/ns#", + "_misskey_content": "misskey:_misskey_content", + "_misskey_quote": "misskey:_misskey_quote", + "_misskey_reaction": "misskey:_misskey_reaction", + "_misskey_votes": "misskey:_misskey_votes", + "_misskey_talk": "misskey:_misskey_talk", + "isCat": "misskey:isCat", + "vcard": "http://www.w3.org/2006/vcard/ns#" + } + ], + "type": "Person", + "id": "https://mk.absturztau.be/users/8ozbzjs3o8", + "inbox": "https://mk.absturztau.be/users/8ozbzjs3o8/inbox", + "outbox": "https://mk.absturztau.be/users/8ozbzjs3o8/outbox", + "followers": "https://mk.absturztau.be/users/8ozbzjs3o8/followers", + "following": "https://mk.absturztau.be/users/8ozbzjs3o8/following", + "featured": "https://mk.absturztau.be/users/8ozbzjs3o8/collections/featured", + "sharedInbox": "https://mk.absturztau.be/inbox", + "endpoints": { + "sharedInbox": "https://mk.absturztau.be/inbox" + }, + "url": "https://mk.absturztau.be/@mametsuko", + "preferredUsername": "mametsuko", + "name": "mametschko", + "summary": "<p><span>nya, ich bin eine Brotperson</span></p>", + "icon": { + "type": "Image", + "url": "https://mk.absturztau.be/files/webpublic-3b5594f4-fa52-4548-b4e3-c379ae2143ed", + "sensitive": false, + "name": null + }, + "image": { + "type": "Image", + "url": "https://mk.absturztau.be/files/webpublic-0d03b03d-b14b-4916-ac3d-8a137118ec84", + "sensitive": false, + "name": null + }, + "tag": [], + "manuallyApprovesFollowers": true, + "discoverable": false, + "publicKey": { + "id": "https://mk.absturztau.be/users/8ozbzjs3o8#main-key", + "type": "Key", + "owner": "https://mk.absturztau.be/users/8ozbzjs3o8", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuN/S1spBGmh8FXI1Bt16\nXB7Cc0QutBp7UPgmDNHjOfsq0zrF4g3L1UBxvrpU0XX77XPMCd9yPvGwAYURH2mv\ntIcYuE+R90VLDmBu5MTVthcG2D874eCZ2rD2YsEYmN5AjTX7QBIqCck+qDhVWkkM\nEZ6S5Ht6IJ5Of74eKffXElQI/C6QB+9uEDOmPk0jCzgI5gw7xvJqFj/DIF4kUUAu\nA89JqaFZzZlkrSrj4cr48bLN/YOmpdaHu0BKHaDSHct4+MqlixqovgdB6RboCEDw\ne4Aeav7+Q0Y9oGIvuggg0Q+nCubnVNnaPyzd817tpPVzyZmTts+DKyDuv90SX3nR\nsPaNa5Ty60eqplUk4b7X1gSvuzBJUFBxTVV84WnjwoeoydaS6rSyjCDPGLBjaByc\nFyWMMEb/zlQyhLZfBlvT7k96wRSsMszh2hDALWmgYIhq/jNwINvALJ1GKLNHHKZ4\nyz2LnxVpRm2rWrZzbvtcnSQOt3LaPSZn8Wgwv4buyHF02iuVuIamZVtKexsE1Ixl\nIi9qa3AKEc5gOzYXhRhvHaruzoCehUbb/UHC5c8Tto8L5G1xYzjLP3qj3PT9w/wM\n+k1Ra/4JhuAnVFROOoOmx9rIELLHH7juY2nhM7plGhyt1M5gysgqEloij8QzyQU2\nZK1YlAERG2XFO6br8omhcmECAwEAAQ==\n-----END PUBLIC KEY-----\n" + }, + "isCat": true, + "vcard:Address": "Vienna, Austria" +}
\ No newline at end of file diff --git a/test/fixtures/tesla_mock/mewmew@lm.kazv.moe.json b/test/fixtures/tesla_mock/mewmew@lm.kazv.moe.json new file mode 100644 index 000000000..8d2c3e1e7 --- /dev/null +++ b/test/fixtures/tesla_mock/mewmew@lm.kazv.moe.json @@ -0,0 +1 @@ +{"@context":["https://www.w3.org/ns/activitystreams","https://lm.kazv.moe/schemas/litepub-0.1.jsonld",{"@language":"und"}],"alsoKnownAs":["https://lm.kazv.moe/users/tester","https://lm.kazv.moe/users/testuser"],"attachment":[],"capabilities":{"acceptsChatMessages":true},"discoverable":false,"endpoints":{"oauthAuthorizationEndpoint":"https://lm.kazv.moe/oauth/authorize","oauthRegistrationEndpoint":"https://lm.kazv.moe/api/v1/apps","oauthTokenEndpoint":"https://lm.kazv.moe/oauth/token","sharedInbox":"https://lm.kazv.moe/inbox","uploadMedia":"https://lm.kazv.moe/api/ap/upload_media"},"featured":"https://lm.kazv.moe/users/mewmew/collections/featured","followers":"https://lm.kazv.moe/users/mewmew/followers","following":"https://lm.kazv.moe/users/mewmew/following","id":"https://lm.kazv.moe/users/mewmew","inbox":"https://lm.kazv.moe/users/mewmew/inbox","manuallyApprovesFollowers":false,"name":"mew","outbox":"https://lm.kazv.moe/users/mewmew/outbox","preferredUsername":"mewmew","publicKey":{"id":"https://lm.kazv.moe/users/mewmew#main-key","owner":"https://lm.kazv.moe/users/mewmew","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0nT3IVUwx799FSJyJEOY\n5D2c5zgtt2Z+BD9417eVLmVQF5fJlWgcKS4pbFc76zkYoBkZtV7XbzvN9KTNulpa\nUGNOM0/UdEoQLB8xbVCMm0ABUU8vbTWoMTxp93bfVHBz+33FPYdH1JHX4TCU/mJF\nX4UJMvFmMn5BFjSQm9GG6Eq2j6SAUsaTa8+Rrd8FzS6zb/dk3N/Llz0tfsZYS0sq\nEy9OYhsKOQ6eegULFJOF3Hz04vzwftmeXFsbb3aO2zKz3uAMYZglWHNBYJAePBtJ\ng362kqdJwgT14TFnZ0K2ziDPbkRULG1Kke/lsqw2rPF6Q6P4PeO1shCEDthoDoID\newIDAQAB\n-----END PUBLIC KEY-----\n\n"},"summary":"","tag":[],"type":"Person","url":"https://lm.kazv.moe/users/mewmew"} diff --git a/test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json b/test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json new file mode 100644 index 000000000..323ca10ed --- /dev/null +++ b/test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json @@ -0,0 +1,44 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "Hashtag": "as:Hashtag", + "quoteUrl": "as:quoteUrl", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "featured": "toot:featured", + "discoverable": "toot:discoverable", + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "misskey": "https://misskey.io/ns#", + "_misskey_content": "misskey:_misskey_content", + "_misskey_quote": "misskey:_misskey_quote", + "_misskey_reaction": "misskey:_misskey_reaction", + "_misskey_votes": "misskey:_misskey_votes", + "_misskey_talk": "misskey:_misskey_talk", + "isCat": "misskey:isCat", + "vcard": "http://www.w3.org/2006/vcard/ns#" + } + ], + "id": "https://misskey.io/notes/8vs6wxufd0", + "type": "Note", + "attributedTo": "https://misskey.io/users/83ssedkv53", + "summary": null, + "content": "<p><span>Fantiaこれできないように過去のやつは従量課金だった気がする</span></p>", + "_misskey_content": "Fantiaこれできないように過去のやつは従量課金だった気がする", + "published": "2022-01-21T16:37:12.663Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://misskey.io/users/83ssedkv53/followers" + ], + "inReplyTo": null, + "attachment": [], + "sensitive": false, + "tag": [] +} diff --git a/test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json b/test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json new file mode 100644 index 000000000..b45ab78e4 --- /dev/null +++ b/test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json @@ -0,0 +1 @@ +{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","Hashtag":"as:Hashtag","quoteUrl":"as:quoteUrl","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji","featured":"toot:featured","discoverable":"toot:discoverable","schema":"http://schema.org#","PropertyValue":"schema:PropertyValue","value":"schema:value","misskey":"https://misskey-hub.net/ns#","_misskey_content":"misskey:_misskey_content","_misskey_quote":"misskey:_misskey_quote","_misskey_reaction":"misskey:_misskey_reaction","_misskey_votes":"misskey:_misskey_votes","_misskey_talk":"misskey:_misskey_talk","isCat":"misskey:isCat","vcard":"http://www.w3.org/2006/vcard/ns#"}],"id":"https://mk.absturztau.be/notes/93e7nm8wqg/activity","actor":"https://mk.absturztau.be/users/8ozbzjs3o8","type":"Create","published":"2022-08-01T11:06:49.568Z","object":{"id":"https://mk.absturztau.be/notes/93e7nm8wqg","type":"Note","attributedTo":"https://mk.absturztau.be/users/8ozbzjs3o8","summary":null,"content":"<p><span>meow</span></p>","_misskey_content":"meow","published":"2022-08-01T11:06:49.568Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://mk.absturztau.be/users/8ozbzjs3o8/followers"],"inReplyTo":null,"attachment":[],"sensitive":false,"tag":[]},"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://mk.absturztau.be/users/8ozbzjs3o8/followers"]}
\ No newline at end of file diff --git a/test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg.json b/test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg.json new file mode 100644 index 000000000..1b931a9a4 --- /dev/null +++ b/test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg.json @@ -0,0 +1,44 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "Hashtag": "as:Hashtag", + "quoteUrl": "as:quoteUrl", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "featured": "toot:featured", + "discoverable": "toot:discoverable", + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "misskey": "https://misskey-hub.net/ns#", + "_misskey_content": "misskey:_misskey_content", + "_misskey_quote": "misskey:_misskey_quote", + "_misskey_reaction": "misskey:_misskey_reaction", + "_misskey_votes": "misskey:_misskey_votes", + "_misskey_talk": "misskey:_misskey_talk", + "isCat": "misskey:isCat", + "vcard": "http://www.w3.org/2006/vcard/ns#" + } + ], + "id": "https://mk.absturztau.be/notes/93e7nm8wqg", + "type": "Note", + "attributedTo": "https://mk.absturztau.be/users/8ozbzjs3o8", + "summary": null, + "content": "<p><span>meow</span></p>", + "_misskey_content": "meow", + "published": "2022-08-01T11:06:49.568Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://mk.absturztau.be/users/8ozbzjs3o8/followers" + ], + "inReplyTo": null, + "attachment": [], + "sensitive": false, + "tag": [] +}
\ No newline at end of file diff --git a/test/fixtures/tesla_mock/p.helene.moe-AM7S6vZQmL6pI9TgPY.json b/test/fixtures/tesla_mock/p.helene.moe-AM7S6vZQmL6pI9TgPY.json new file mode 100644 index 000000000..a1ef5e20b --- /dev/null +++ b/test/fixtures/tesla_mock/p.helene.moe-AM7S6vZQmL6pI9TgPY.json @@ -0,0 +1,36 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://p.helene.moe/schemas/litepub-0.1.jsonld", + { + "@language": "und" + } + ], + "actor": "https://p.helene.moe/users/helene", + "attachment": [], + "attributedTo": "https://p.helene.moe/users/helene", + "cc": [ + "https://p.helene.moe/users/helene/followers" + ], + "content": "<span class=\"h-card\"><a class=\"u-url mention\" data-user=\"AHntpQ4T3J4OSnpgMC\" href=\"https://mk.absturztau.be/@mametsuko\" rel=\"ugc\">@<span>mametsuko</span></a></span> meow", + "context": "https://p.helene.moe/contexts/cc324643-5583-4c3f-91d2-c6ed37db159d", + "conversation": "https://p.helene.moe/contexts/cc324643-5583-4c3f-91d2-c6ed37db159d", + "id": "https://p.helene.moe/objects/fd5910ac-d9dc-412e-8d1d-914b203296c4", + "inReplyTo": "https://mk.absturztau.be/notes/93e7nm8wqg", + "published": "2022-08-02T13:46:58.403996Z", + "sensitive": null, + "source": "@mametsuko@mk.absturztau.be meow", + "summary": "", + "tag": [ + { + "href": "https://mk.absturztau.be/users/8ozbzjs3o8", + "name": "@mametsuko@mk.absturztau.be", + "type": "Mention" + } + ], + "to": [ + "https://mk.absturztau.be/users/8ozbzjs3o8", + "https://www.w3.org/ns/activitystreams#Public" + ], + "type": "Note" +}
\ No newline at end of file diff --git a/test/fixtures/tesla_mock/smithereen_non_anonymous_poll.json b/test/fixtures/tesla_mock/smithereen_non_anonymous_poll.json new file mode 100644 index 000000000..2b343ea64 --- /dev/null +++ b/test/fixtures/tesla_mock/smithereen_non_anonymous_poll.json @@ -0,0 +1 @@ +{"type":"Question","id":"https://friends.grishka.me/posts/54642","attributedTo":"https://friends.grishka.me/users/1","content":"<p>здесь тоже можно что-то написать отдельно от опроса</p>","published":"2021-09-04T00:22:16Z","url":"https://friends.grishka.me/posts/54642","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://friends.grishka.me/users/1/followers"],"replies":{"type":"Collection","id":"https://friends.grishka.me/posts/54642/replies","first":{"type":"CollectionPage","items":[],"partOf":"https://friends.grishka.me/posts/54642/replies","next":"https://friends.grishka.me/posts/54642/replies?page=1"}},"sensitive":false,"likes":"https://friends.grishka.me/posts/54642/likes","name":"тестовый опрос","oneOf":[{"type":"Note","id":"https://friends.grishka.me/posts/54642#options/76","name":"тестовый ответ 1","replies":{"type":"Collection","id":"https://friends.grishka.me/activitypub/objects/polls/24/options/76/votes","totalItems":4,"items":[]}},{"type":"Note","id":"https://friends.grishka.me/posts/54642#options/77","name":"тестовый ответ 2","replies":{"type":"Collection","id":"https://friends.grishka.me/activitypub/objects/polls/24/options/77/votes","totalItems":4,"items":[]}},{"type":"Note","id":"https://friends.grishka.me/posts/54642#options/78","name":"тестовый ответ 3","replies":{"type":"Collection","id":"https://friends.grishka.me/activitypub/objects/polls/24/options/78/votes","totalItems":6,"items":[]}}],"votersCount":14,"nonAnonymous":true,"@context":["https://www.w3.org/ns/activitystreams",{"sensitive":"as:sensitive","toot":"http://joinmastodon.org/ns#","sm":"http://smithereen.software/ns#","votersCount":"toot:votersCount","nonAnonymous":"sm:nonAnonymous"}]}
\ No newline at end of file diff --git a/test/fixtures/tesla_mock/smithereen_user.json b/test/fixtures/tesla_mock/smithereen_user.json new file mode 100644 index 000000000..6468fc519 --- /dev/null +++ b/test/fixtures/tesla_mock/smithereen_user.json @@ -0,0 +1 @@ +{"type":"Person","id":"https://friends.grishka.me/users/1","name":"Григорий Клюшников","icon":{"type":"Image","image":{"type":"Image","url":"https://friends.grishka.me/i/6QLsOws97AWp5N_osd74C1IC1ijnFopyCBD9MSEeXNQ/q:93/bG9jYWw6Ly8vcy91cGxvYWRzL2F2YXRhcnMvNTYzODRhODEwODk5ZTRjMzI4YmY4YmQwM2Q2MWM3NmMud2VicA.jpg","mediaType":"image/jpeg","width":1280,"height":960},"width":573,"height":572,"cropRegion":[0.26422762870788574,0.3766937553882599,0.7113820910453796,0.9728997349739075],"url":"https://friends.grishka.me/i/ql_49PQcETAWgY_nC-Qj63H_Oa6FyOAEoWFkUSSkUvQ/c:573:572:nowe:338:362/q:93/bG9jYWw6Ly8vcy91cGxvYWRzL2F2YXRhcnMvNTYzODRhODEwODk5ZTRjMzI4YmY4YmQwM2Q2MWM3NmMud2VicA.jpg","mediaType":"image/jpeg"},"summary":"<p>Делаю эту хрень, пытаюсь вырвать социальные сети из жадных лап корпораций</p>\n<p></p>\n<p></p>\n<p></p>\n<p></p>\n<p></p>\n<p></p>\n<p></p>\n<p>This server does NOT support direct messages. Please write me <a href=\"https://t.me/grishka\">on Telegram</a> or <a href=\"https://matrix.to/#/@grishk:matrix.org\">Matrix</a>.</p>","url":"https://friends.grishka.me/grishka","preferredUsername":"grishka","inbox":"https://friends.grishka.me/users/1/inbox","outbox":"https://friends.grishka.me/users/1/outbox","followers":"https://friends.grishka.me/users/1/followers","following":"https://friends.grishka.me/users/1/following","endpoints":{"sharedInbox":"https://friends.grishka.me/activitypub/sharedInbox","collectionSimpleQuery":"https://friends.grishka.me/users/1/collectionQuery"},"publicKey":{"id":"https://friends.grishka.me/users/1#main-key","owner":"https://friends.grishka.me/users/1","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjlakm+i/d9ER/hIeR7KfiFW+SdLZj2SkKIeM8cmR+YFJuh9ghFqXrkFEjcaqUnAFqe5gYDNSQACnDLA8y4DnzjfGNIohKAnRoa9x6GORmfKQvcnjaTZ53S1NvUiPPyc0Pv/vfCtY/Ab0CEXe5BLqL38oZn817Jf7pBrPRTYH7m012kvwAUTT6k0Y8lPITBEG7nzYbbuGcrN9Y/RDdwE08jmBXlZ45bahRH3VNXVpQE17dCzJB+7k+iJ1R7YCoI+DuMlBYGXGE2KVk46NZTuLnOjFV9SyXfWX4/SrJM4oxev+SX2N75tQgmNZmVVHeqg2ZcbC0WCfNjJOi2HHS9MujwIDAQAB\n-----END PUBLIC KEY-----\n"},"wall":"https://friends.grishka.me/users/1/wall","firstName":"Григорий","lastName":"Клюшников","middleName":"Александрович","vcard:bday":"1993-01-22","gender":"http://schema.org#Male","supportsFriendRequests":true,"friends":"https://friends.grishka.me/users/1/friends","groups":"https://friends.grishka.me/users/1/groups","capabilities":{"supportsFriendRequests":true},"@context":["https://www.w3.org/ns/activitystreams",{"sm":"http://smithereen.software/ns#","cropRegion":{"@id":"sm:cropRegion","@container":"@list"},"wall":{"@id":"sm:wall","@type":"@id"},"collectionSimpleQuery":"sm:collectionSimpleQuery","sc":"http://schema.org#","firstName":"sc:givenName","lastName":"sc:familyName","middleName":"sc:additionalName","gender":{"@id":"sc:gender","@type":"sc:GenderType"},"maidenName":"sm:maidenName","friends":{"@id":"sm:friends","@type":"@id"},"groups":{"@id":"sm:groups","@type":"@id"},"vcard":"http://www.w3.org/2006/vcard/ns#","capabilities":"litepub:capabilities","supportsFriendRequests":"sm:supportsFriendRequests","litepub":"http://litepub.social/ns#"},"https://w3id.org/security/v1"]}
\ No newline at end of file diff --git a/test/fixtures/tesla_mock/status.alpicola.com_host_meta b/test/fixtures/tesla_mock/status.alpicola.com_host_meta index 6948c30ea..78155f644 100644 --- a/test/fixtures/tesla_mock/status.alpicola.com_host_meta +++ b/test/fixtures/tesla_mock/status.alpicola.com_host_meta @@ -1,2 +1,2 @@ <?xml version="1.0" encoding="UTF-8"?> -<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><hm:Host xmlns:hm="http://host-meta.net/xrd/1.0">status.alpicola.com</hm:Host><Link rel="lrdd" template="http://status.alpicola.com/main/xrd?uri={uri}"><Title>Resource Descriptor</Title></Link></XRD>
\ No newline at end of file +<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><hm:Host xmlns:hm="http://host-meta.net/xrd/1.0">status.alpicola.com</hm:Host><Link rel="lrdd" template="https://status.alpicola.com/main/xrd?uri={uri}"><Title>Resource Descriptor</Title></Link></XRD> diff --git a/test/fixtures/tesla_mock/webfinger_spoof.json b/test/fixtures/tesla_mock/webfinger_spoof.json new file mode 100644 index 000000000..7c2a11f69 --- /dev/null +++ b/test/fixtures/tesla_mock/webfinger_spoof.json @@ -0,0 +1,28 @@ +{ + "aliases": [ + "https://gleasonator.com/users/alex", + "https://mitra.social/users/alex" + ], + "links": [ + { + "href": "https://gleasonator.com/users/alex", + "rel": "http://webfinger.net/rel/profile-page", + "type": "text/html" + }, + { + "href": "https://gleasonator.com/users/alex", + "rel": "self", + "type": "application/activity+json" + }, + { + "href": "https://gleasonator.com/users/alex", + "rel": "self", + "type": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + }, + { + "rel": "http://ostatus.org/schema/1.0/subscribe", + "template": "https://gleasonator.com/ostatus_subscribe?acct={uri}" + } + ], + "subject": "acct:trump@whitehouse.gov" +} diff --git a/test/fixtures/webfinger/graf-imposter-webfinger.json b/test/fixtures/webfinger/graf-imposter-webfinger.json new file mode 100644 index 000000000..e7010f606 --- /dev/null +++ b/test/fixtures/webfinger/graf-imposter-webfinger.json @@ -0,0 +1,41 @@ +{ + "subject": "acct:graf@poa.st", + "aliases": [ + "https://fba.ryona.agenc/webfingertest" + ], + "links": [ + { + "rel": "http://webfinger.net/rel/profile-page", + "type": "text/html", + "href": "https://fba.ryona.agenc/webfingertest" + }, + { + "rel": "self", + "type": "application/activity+json", + "href": "https://fba.ryona.agenc/webfingertest" + }, + { + "rel": "http://ostatus.org/schema/1.0/subscribe", + "template": "https://fba.ryona.agenc/contact/follow?url={uri}" + }, + { + "rel": "http://schemas.google.com/g/2010#updates-from", + "type": "application/atom+xml", + "href": "" + }, + { + "rel": "salmon", + "href": "https://fba.ryona.agenc/salmon/friendica" + }, + { + "rel": "http://microformats.org/profile/hcard", + "type": "text/html", + "href": "https://fba.ryona.agenc/hcard/friendica" + }, + { + "rel": "http://joindiaspora.com/seed_location", + "type": "text/html", + "href": "https://fba.ryona.agenc" + } + ] +} diff --git a/test/fixtures/webfinger/masto-host-meta.xml b/test/fixtures/webfinger/masto-host-meta.xml new file mode 100644 index 000000000..f432a27c3 --- /dev/null +++ b/test/fixtures/webfinger/masto-host-meta.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"> + <Link rel="lrdd" template="https://{{domain}}/.well-known/webfinger?resource={uri}"/> +</XRD> diff --git a/test/fixtures/webfinger/masto-user.json b/test/fixtures/webfinger/masto-user.json new file mode 100644 index 000000000..1702de011 --- /dev/null +++ b/test/fixtures/webfinger/masto-user.json @@ -0,0 +1,92 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "toot": "http://joinmastodon.org/ns#", + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "featuredTags": { + "@id": "toot:featuredTags", + "@type": "@id" + }, + "alsoKnownAs": { + "@id": "as:alsoKnownAs", + "@type": "@id" + }, + "movedTo": { + "@id": "as:movedTo", + "@type": "@id" + }, + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "IdentityProof": "toot:IdentityProof", + "discoverable": "toot:discoverable", + "Device": "toot:Device", + "Ed25519Signature": "toot:Ed25519Signature", + "Ed25519Key": "toot:Ed25519Key", + "Curve25519Key": "toot:Curve25519Key", + "EncryptedMessage": "toot:EncryptedMessage", + "publicKeyBase64": "toot:publicKeyBase64", + "deviceId": "toot:deviceId", + "claim": { + "@type": "@id", + "@id": "toot:claim" + }, + "fingerprintKey": { + "@type": "@id", + "@id": "toot:fingerprintKey" + }, + "identityKey": { + "@type": "@id", + "@id": "toot:identityKey" + }, + "devices": { + "@type": "@id", + "@id": "toot:devices" + }, + "messageFranking": "toot:messageFranking", + "messageType": "toot:messageType", + "cipherText": "toot:cipherText", + "suspended": "toot:suspended", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + } + } + ], + "id": "https://{{domain}}/users/{{nickname}}", + "type": "Person", + "following": "https://{{domain}}/users/{{nickname}}/following", + "followers": "https://{{domain}}/users/{{nickname}}/followers", + "inbox": "https://{{domain}}/users/{{nickname}}/inbox", + "outbox": "https://{{domain}}/users/{{nickname}}/outbox", + "featured": "https://{{domain}}/users/{{nickname}}/collections/featured", + "featuredTags": "https://{{domain}}/users/{{nickname}}/collections/tags", + "preferredUsername": "{{nickname}}", + "name": "Name Name", + "summary": "<p>Summary</p>", + "url": "https://{{domain}}/@{{nickname}}", + "manuallyApprovesFollowers": false, + "discoverable": false, + "devices": "https://{{domain}}/users/{{nickname}}/collections/devices", + "publicKey": { + "id": "https://{{domain}}/users/{{nickname}}#main-key", + "owner": "https://{{domain}}/users/{{nickname}}", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvwDujxmxoYHs64MyVB3L\nG5ZyBxV3ufaMRBFu42bkcTpISq1WwZ+3Zb6CI8zOO+nM+Q2llrVRYjZa4ZFnOLvM\nTq/Kf+Zf5wy2aCRer88gX+MsJOAtItSi412y0a/rKOuFaDYLOLeTkRvmGLgZWbsr\nZJOp+YWb3zQ5qsIOInkc5BwI172tMsGeFtsnbNApPV4lrmtTGaJ8RiM8MR7XANBO\nfOHggSt1+eAIKGIsCmINEMzs1mG9D75xKtC/sM8GfbvBclQcBstGkHAEj1VHPW0c\nh6Bok5/QQppicyb8UA1PAA9bznSFtKlYE4xCH8rlCDSDTBRtdnBWHKcj619Ujz4Q\nawIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "tag": [], + "attachment": [], + "endpoints": { + "sharedInbox": "https://{{domain}}/inbox" + }, + "icon": { + "type": "Image", + "mediaType": "image/jpeg", + "url": "https://s3.wasabisys.com/merp/accounts/avatars/000/000/001/original/6fdd3eee632af247.jpg" + } +} diff --git a/test/fixtures/webfinger/masto-webfinger.json b/test/fixtures/webfinger/masto-webfinger.json new file mode 100644 index 000000000..561be3fff --- /dev/null +++ b/test/fixtures/webfinger/masto-webfinger.json @@ -0,0 +1,23 @@ +{ + "subject": "acct:{{nickname}}@{{domain}}", + "aliases": [ + "https://{{subdomain}}/@{{nickname}}", + "https://{{subdomain}}/users/{{nickname}}" + ], + "links": [ + { + "rel": "http://webfinger.net/rel/profile-page", + "type": "text/html", + "href": "https://{{subdomain}}/@{{nickname}}" + }, + { + "rel": "self", + "type": "application/activity+json", + "href": "https://{{subdomain}}/users/{{nickname}}" + }, + { + "rel": "http://ostatus.org/schema/1.0/subscribe", + "template": "https://{{subdomain}}/authorize_interaction?uri={uri}" + } + ] +} diff --git a/test/fixtures/webfinger/pleroma-host-meta.xml b/test/fixtures/webfinger/pleroma-host-meta.xml new file mode 100644 index 000000000..88c274a1a --- /dev/null +++ b/test/fixtures/webfinger/pleroma-host-meta.xml @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" template="https://{{domain}}/.well-known/webfinger?resource={uri}" type="application/xrd+xml" /></XRD> diff --git a/test/fixtures/webfinger/pleroma-user.json b/test/fixtures/webfinger/pleroma-user.json new file mode 100644 index 000000000..b822db46c --- /dev/null +++ b/test/fixtures/webfinger/pleroma-user.json @@ -0,0 +1,58 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://{{domain}}/schemas/litepub-0.1.jsonld", + { + "@language": "und" + } + ], + "alsoKnownAs": [], + "attachment": [], + "capabilities": { + "acceptsChatMessages": true + }, + "discoverable": true, + "endpoints": { + "oauthAuthorizationEndpoint": "https://{{domain}}/oauth/authorize", + "oauthRegistrationEndpoint": "https://{{domain}}/api/v1/apps", + "oauthTokenEndpoint": "https://{{domain}}/oauth/token", + "sharedInbox": "https://{{domain}}/inbox", + "uploadMedia": "https://{{domain}}/api/ap/upload_media" + }, + "followers": "https://{{domain}}/users/{{nickname}}/followers", + "following": "https://{{domain}}/users/{{nickname}}/following", + "icon": { + "type": "Image", + "url": "https://{{domain}}/media/a932a27f158b63c3a97e3a57d5384f714a82249274c6fc66c9eca581b4fd8af2.jpg" + }, + "id": "https://{{domain}}/users/{{nickname}}", + "image": { + "type": "Image", + "url": "https://{{domain}}/media/db15f476d0ad14488db4762b7800479e6ef67b1824f8b9ea5c1fa05b7525c5b7.jpg" + }, + "inbox": "https://{{domain}}/users/{{nickname}}/inbox", + "manuallyApprovesFollowers": false, + "name": "{{nickname}} :verified:", + "outbox": "https://{{domain}}/users/{{nickname}}/outbox", + "preferredUsername": "{{nickname}}", + "publicKey": { + "id": "https://{{domain}}/users/{{nickname}}#main-key", + "owner": "https://{{domain}}/users/{{nickname}}", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu4XOAopC4nRIxNlHlt60\n//nCicuedu5wvLGIoQ+KUM2u7/PhLrrTDEqr1A7yQL95S0X8ryYtALgFLI5A54ww\nqjMIbIGAs44lEmDLMEd+XI+XxREE8wdsFpb4QQzWug0DTyqlMouTU25k0tfKh1rF\n4PMJ3uBSjDTAGgFvLNyFWTiVVgChbTNgGOmrEBucRl4NmKzQ69/FIUwENV88oQSU\n3bWvQTEH9rWH1rCLpkmQwdRiWfnhFX/4EUqXukfgoskvenKR8ff3nYhElDqFoE0e\nqUnIW1OZceyl8JewVLcL6m0/wdKeosTsfrcWc8DKfnRYQcBGNoBEq9GrOHDU0q2v\nyQIDAQAB\n-----END PUBLIC KEY-----\n\n" + }, + "summary": "Pleroma BE dev", + "tag": [ + { + "icon": { + "type": "Image", + "url": "https://{{domain}}/emoji/mine/6143373a807b1ae7.png" + }, + "id": "https://{{domain}}/emoji/mine/6143373a807b1ae7.png", + "name": ":verified:", + "type": "Emoji", + "updated": "1970-01-01T00:00:00Z" + } + ], + "type": "Person", + "url": "https://{{domain}}/users/{{nickname}}" +} diff --git a/test/fixtures/webfinger/pleroma-webfinger.json b/test/fixtures/webfinger/pleroma-webfinger.json new file mode 100644 index 000000000..8f075eaaf --- /dev/null +++ b/test/fixtures/webfinger/pleroma-webfinger.json @@ -0,0 +1,27 @@ +{ + "aliases": [ + "https://{{subdomain}}/users/{{nickname}}" + ], + "links": [ + { + "href": "https://{{subdomain}}/users/{{nickname}}", + "rel": "http://webfinger.net/rel/profile-page", + "type": "text/html" + }, + { + "href": "https://{{subdomain}}/users/{{nickname}}", + "rel": "self", + "type": "application/activity+json" + }, + { + "href": "https://{{subdomain}}/users/{{nickname}}", + "rel": "self", + "type": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" + }, + { + "rel": "http://ostatus.org/schema/1.0/subscribe", + "template": "https://{{subdomain}}/ostatus_subscribe?acct={uri}" + } + ], + "subject": "acct:{{nickname}}@{{domain}}" +} diff --git a/test/fixtures/wildebeest-nodeinfo21.json b/test/fixtures/wildebeest-nodeinfo21.json new file mode 100644 index 000000000..c6af474bf --- /dev/null +++ b/test/fixtures/wildebeest-nodeinfo21.json @@ -0,0 +1 @@ +{"version":"2.1","software":{"name":"wildebeest","version":"0.0.1","repository":"https://github.com/cloudflare/wildebeest"},"protocols":["activitypub"],"usage":{"users":{"total":1,"activeMonth":1,"activeHalfyear":1}},"openRegistrations":false,"metadata":{"upstream":{"name":"mastodon","version":"3.5.1"}}}
\ No newline at end of file diff --git a/test/fixtures/wildebeest-well-known-nodeinfo.json b/test/fixtures/wildebeest-well-known-nodeinfo.json new file mode 100644 index 000000000..c7ddb43af --- /dev/null +++ b/test/fixtures/wildebeest-well-known-nodeinfo.json @@ -0,0 +1 @@ +{"links":[{"rel":"http://nodeinfo.diaspora.software/ns/schema/2.0","href":"https://wildebeest.example.org/nodeinfo/2.0"},{"rel":"http://nodeinfo.diaspora.software/ns/schema/2.1","href":"https://wildebeest.example.org/nodeinfo/2.1"}]}
\ No newline at end of file diff --git a/test/fixtures/xml_billion_laughs.xml b/test/fixtures/xml_billion_laughs.xml new file mode 100644 index 000000000..75fb24cae --- /dev/null +++ b/test/fixtures/xml_billion_laughs.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!DOCTYPE lolz [ + <!ENTITY lol "lol"> + <!ELEMENT lolz (#PCDATA)> + <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> + <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;"> + <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;"> + <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;"> + <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;"> + <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;"> + <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;"> + <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;"> + <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;"> +]> +<lolz>&lol9;</lolz> diff --git a/test/fixtures/xml_external_entities.xml b/test/fixtures/xml_external_entities.xml new file mode 100644 index 000000000..d5ff87134 --- /dev/null +++ b/test/fixtures/xml_external_entities.xml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> +<stockCheck><productId>&xxe;</productId></stockCheck> diff --git a/test/mix/pleroma_test.exs b/test/mix/pleroma_test.exs index c981ee9b9..e362223b2 100644 --- a/test/mix/pleroma_test.exs +++ b/test/mix/pleroma_test.exs @@ -39,7 +39,7 @@ defmodule Mix.PleromaTest do describe "get_option/3" do test "get from options" do - assert get_option([domain: "some-domain.com"], :domain, "Promt") == "some-domain.com" + assert get_option([domain: "some-domain.com"], :domain, "Prompt") == "some-domain.com" end test "get from prompt" do diff --git a/test/mix/tasks/pleroma/config_test.exs b/test/mix/tasks/pleroma/config_test.exs index f90ef8804..7b2134129 100644 --- a/test/mix/tasks/pleroma/config_test.exs +++ b/test/mix/tasks/pleroma/config_test.exs @@ -49,7 +49,6 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do describe "migrate_to_db/1" do setup do clear_config(:configurable_from_database, true) - clear_config([:quack, :level]) end @tag capture_log: true @@ -72,14 +71,12 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do config1 = ConfigDB.get_by_params(%{group: ":pleroma", key: ":first_setting"}) config2 = ConfigDB.get_by_params(%{group: ":pleroma", key: ":second_setting"}) - config3 = ConfigDB.get_by_params(%{group: ":quack", key: ":level"}) refute ConfigDB.get_by_params(%{group: ":pleroma", key: "Pleroma.Repo"}) refute ConfigDB.get_by_params(%{group: ":postgrex", key: ":json_library"}) refute ConfigDB.get_by_params(%{group: ":pleroma", key: ":database"}) assert config1.value == [key: "value", key2: [Repo]] assert config2.value == [key: "value2", key2: ["Activity"]] - assert config3.value == :info end test "config table is truncated before migration" do @@ -108,7 +105,6 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do test "settings are migrated to file and deleted from db", %{temp_file: temp_file} do insert_config_record(:pleroma, :setting_first, key: "value", key2: ["Activity"]) insert_config_record(:pleroma, :setting_second, key: "value2", key2: [Repo]) - insert_config_record(:quack, :level, :info) MixTask.run(["migrate_from_db", "--env", "temp", "-d"]) @@ -117,7 +113,6 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do file = File.read!(temp_file) assert file =~ "config :pleroma, :setting_first," assert file =~ "config :pleroma, :setting_second," - assert file =~ "config :quack, :level, :info" end test "load a settings with large values and pass to file", %{temp_file: temp_file} do @@ -145,7 +140,6 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do federating: true, federation_incoming_replies_max_depth: 100, federation_reachability_timeout_days: 7, - federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher], allow_relay: true, public: true, quarantined_instances: [], @@ -188,8 +182,8 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do assert File.exists?(temp_file) {:ok, file} = File.read(temp_file) - assert file == - "import Config\n\nconfig :pleroma, :instance,\n name: \"Pleroma\",\n email: \"example@example.com\",\n notify_email: \"noreply@example.com\",\n description: \"A Pleroma instance, an alternative fediverse server\",\n limit: 5000,\n chat_limit: 5000,\n remote_limit: 100_000,\n upload_limit: 16_000_000,\n avatar_upload_limit: 2_000_000,\n background_upload_limit: 4_000_000,\n banner_upload_limit: 4_000_000,\n poll_limits: %{\n max_expiration: 31_536_000,\n max_option_chars: 200,\n max_options: 20,\n min_expiration: 0\n },\n registrations_open: true,\n federating: true,\n federation_incoming_replies_max_depth: 100,\n federation_reachability_timeout_days: 7,\n federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],\n allow_relay: true,\n public: true,\n quarantined_instances: [],\n managed_config: true,\n static_dir: \"instance/static/\",\n allowed_post_formats: [\"text/plain\", \"text/html\", \"text/markdown\", \"text/bbcode\"],\n autofollowed_nicknames: [],\n max_pinned_statuses: 1,\n attachment_links: false,\n max_report_comment_size: 1000,\n safe_dm_mentions: false,\n healthcheck: false,\n remote_post_retention_days: 90,\n skip_thread_containment: true,\n limit_to_local_content: :unauthenticated,\n user_bio_length: 5000,\n user_name_length: 100,\n max_account_fields: 10,\n max_remote_account_fields: 20,\n account_field_name_length: 512,\n account_field_value_length: 2048,\n external_user_synchronization: true,\n extended_nickname_format: true,\n multi_factor_authentication: [\n totp: [digits: 6, period: 30],\n backup_codes: [number: 2, length: 6]\n ]\n" + assert file =~ "import Config\n" + assert file =~ "A Pleroma instance, an alternative fediverse server" end end @@ -199,7 +193,6 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do setup do insert_config_record(:pleroma, :setting_first, key: "value", key2: ["Activity"]) insert_config_record(:pleroma, :setting_second, key: "value2", key2: [Repo]) - insert_config_record(:quack, :level, :info) path = "test/instance_static" file_path = Path.join(path, "temp.exported_from_db.secret.exs") @@ -215,7 +208,6 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do file = File.read!(file_path) assert file =~ "config :pleroma, :setting_first," assert file =~ "config :pleroma, :setting_second," - assert file =~ "config :quack, :level, :info" end test "release", %{file_path: file_path} do @@ -227,7 +219,6 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do file = File.read!(file_path) assert file =~ "config :pleroma, :setting_first," assert file =~ "config :pleroma, :setting_second," - assert file =~ "config :quack, :level, :info" end end diff --git a/test/mix/tasks/pleroma/digest_test.exs b/test/mix/tasks/pleroma/digest_test.exs index d2a8606c7..08482aadb 100644 --- a/test/mix/tasks/pleroma/digest_test.exs +++ b/test/mix/tasks/pleroma/digest_test.exs @@ -23,6 +23,11 @@ defmodule Mix.Tasks.Pleroma.DigestTest do setup do: clear_config([Pleroma.Emails.Mailer, :enabled], true) + setup do + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + :ok + end + describe "pleroma.digest test" do test "Sends digest to the given user" do user1 = insert(:user) diff --git a/test/mix/tasks/pleroma/ecto/migrate_test.exs b/test/mix/tasks/pleroma/ecto/migrate_test.exs index 912471f60..936e5382a 100644 --- a/test/mix/tasks/pleroma/ecto/migrate_test.exs +++ b/test/mix/tasks/pleroma/ecto/migrate_test.exs @@ -9,7 +9,7 @@ defmodule Mix.Tasks.Pleroma.Ecto.MigrateTest do test "ecto.migrate info message" do level = Logger.level() - Logger.configure(level: :warn) + Logger.configure(level: :warning) assert capture_log(fn -> Mix.Tasks.Pleroma.Ecto.Migrate.run() diff --git a/test/mix/tasks/pleroma/ecto/rollback_test.exs b/test/mix/tasks/pleroma/ecto/rollback_test.exs index 9d1a02ae2..4036b2da6 100644 --- a/test/mix/tasks/pleroma/ecto/rollback_test.exs +++ b/test/mix/tasks/pleroma/ecto/rollback_test.exs @@ -9,11 +9,11 @@ defmodule Mix.Tasks.Pleroma.Ecto.RollbackTest do test "ecto.rollback info message" do level = Logger.level() - Logger.configure(level: :warn) + Logger.configure(level: :warning) assert capture_log(fn -> Mix.Tasks.Pleroma.Ecto.Rollback.run(["--env", "test"]) - end) =~ "[info] Rollback succesfully" + end) =~ "[info] Rollback successfully" Logger.configure(level: level) end diff --git a/test/mix/tasks/pleroma/openapi_spec_test.exs b/test/mix/tasks/pleroma/openapi_spec_test.exs new file mode 100644 index 000000000..01437187a --- /dev/null +++ b/test/mix/tasks/pleroma/openapi_spec_test.exs @@ -0,0 +1,62 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Mix.Tasks.Pleroma.OpenapiSpecTest do + use Pleroma.DataCase, async: true + + alias Mix.Tasks.Pleroma.OpenapiSpec + + @spec_base %{ + "paths" => %{ + "/cofe" => %{ + "get" => %{ + "operationId" => "Some.operation", + "tags" => [] + } + }, + "/mew" => %{ + "post" => %{ + "operationId" => "Another.operation", + "tags" => ["mew mew"] + } + } + }, + "x-tagGroups" => [ + %{ + "name" => "mew", + "tags" => ["mew mew", "abc"] + }, + %{ + "name" => "lol", + "tags" => ["lol lol", "xyz"] + } + ] + } + + describe "check_specs/1" do + test "Every operation must have a tag" do + assert {:error, ["Some.operation (get /cofe): No tags specified"]} == + OpenapiSpec.check_specs(@spec_base) + end + + test "Every tag must be in tag groups" do + spec = + @spec_base + |> put_in(["paths", "/cofe", "get", "tags"], ["abc", "def", "not specified"]) + + assert {:error, + [ + "Some.operation (get /cofe): Tags #{inspect(["def", "not specified"])} not available. Please add it in \"x-tagGroups\" in Pleroma.Web.ApiSpec" + ]} == OpenapiSpec.check_specs(spec) + end + + test "No errors if ok" do + spec = + @spec_base + |> put_in(["paths", "/cofe", "get", "tags"], ["abc", "mew mew"]) + + assert :ok == OpenapiSpec.check_specs(spec) + end + end +end diff --git a/test/mix/tasks/pleroma/robots_txt_test.exs b/test/mix/tasks/pleroma/robots_txt_test.exs index 4426fe526..dd6ca9fc8 100644 --- a/test/mix/tasks/pleroma/robots_txt_test.exs +++ b/test/mix/tasks/pleroma/robots_txt_test.exs @@ -26,7 +26,7 @@ defmodule Mix.Tasks.Pleroma.RobotsTxtTest do assert file == "User-Agent: *\nDisallow: /\n" end - test "to existance folder" do + test "to existing folder" do path = "test/fixtures/" file_path = path <> "robots.txt" clear_config([:instance, :static_dir], path) diff --git a/test/mix/tasks/pleroma/user_test.exs b/test/mix/tasks/pleroma/user_test.exs index 4fdf6912b..c9bcf2951 100644 --- a/test/mix/tasks/pleroma/user_test.exs +++ b/test/mix/tasks/pleroma/user_test.exs @@ -20,6 +20,11 @@ defmodule Mix.Tasks.Pleroma.UserTest do import Mock import Pleroma.Factory + setup do + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + :ok + end + setup_all do Mix.shell(Mix.Shell.Process) diff --git a/test/pleroma/activity/ir/topics_test.exs b/test/pleroma/activity/ir/topics_test.exs index 311f85dea..36a6ca026 100644 --- a/test/pleroma/activity/ir/topics_test.exs +++ b/test/pleroma/activity/ir/topics_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Activity.Ir.TopicsTest do - use Pleroma.DataCase, async: true + use Pleroma.DataCase alias Pleroma.Activity alias Pleroma.Activity.Ir.Topics @@ -13,6 +13,29 @@ defmodule Pleroma.Activity.Ir.TopicsTest do import Mock + describe "chat message" do + test "Create produces no topics" do + activity = %Activity{ + object: %Object{data: %{"type" => "ChatMessage"}}, + data: %{"type" => "Create"} + } + + assert [] == Topics.get_activity_topics(activity) + end + + test "Delete produces user and user:pleroma_chat" do + activity = %Activity{ + object: %Object{data: %{"type" => "ChatMessage"}}, + data: %{"type" => "Delete"} + } + + topics = Topics.get_activity_topics(activity) + assert [_, _] = topics + assert "user" in topics + assert "user:pleroma_chat" in topics + end + end + describe "poll answer" do test "produce no topics" do activity = %Activity{object: %Object{data: %{"type" => "Answer"}}} @@ -35,7 +58,7 @@ defmodule Pleroma.Activity.Ir.TopicsTest do setup do activity = %Activity{ object: %Object{data: %{"type" => "Note"}}, - data: %{"to" => [Pleroma.Constants.as_public()]} + data: %{"to" => [Pleroma.Constants.as_public()], "type" => "Create"} } {:ok, activity: activity} @@ -114,6 +137,55 @@ defmodule Pleroma.Activity.Ir.TopicsTest do end end + describe "public visibility Announces" do + setup do + activity = %Activity{ + object: %Object{data: %{"attachment" => []}}, + data: %{"type" => "Announce", "to" => [Pleroma.Constants.as_public()]} + } + + {:ok, activity: activity} + end + + test "does not generate public topics", %{activity: activity} do + topics = Topics.get_activity_topics(activity) + + refute "public" in topics + refute "public:remote" in topics + refute "public:local" in topics + end + end + + describe "local-public visibility create events" do + setup do + activity = %Activity{ + object: %Object{data: %{"attachment" => []}}, + data: %{"type" => "Create", "to" => [Pleroma.Web.ActivityPub.Utils.as_local_public()]} + } + + {:ok, activity: activity} + end + + test "doesn't produce public topics", %{activity: activity} do + topics = Topics.get_activity_topics(activity) + + refute Enum.member?(topics, "public") + end + + test "produces public:local topics", %{activity: activity} do + topics = Topics.get_activity_topics(activity) + + assert Enum.member?(topics, "public:local") + end + + test "with no attachments doesn't produce public:media topics", %{activity: activity} do + topics = Topics.get_activity_topics(activity) + + refute Enum.member?(topics, "public:media") + refute Enum.member?(topics, "public:local:media") + end + end + describe "public visibility create events with attachments" do setup do activity = %Activity{ @@ -152,9 +224,36 @@ defmodule Pleroma.Activity.Ir.TopicsTest do end end + describe "local-public visibility create events with attachments" do + setup do + activity = %Activity{ + object: %Object{data: %{"attachment" => ["foo"]}}, + data: %{"type" => "Create", "to" => [Pleroma.Web.ActivityPub.Utils.as_local_public()]} + } + + {:ok, activity: activity} + end + + test "do not produce public:media topics", %{activity: activity} do + topics = Topics.get_activity_topics(activity) + + refute Enum.member?(topics, "public:media") + end + + test "produces public:local:media topics", %{activity: activity} do + topics = Topics.get_activity_topics(activity) + + assert Enum.member?(topics, "public:local:media") + end + end + describe "non-public visibility" do test "produces direct topic" do - activity = %Activity{object: %Object{data: %{"type" => "Note"}}, data: %{"to" => []}} + activity = %Activity{ + object: %Object{data: %{"type" => "Note"}}, + data: %{"to" => [], "type" => "Create"} + } + topics = Topics.get_activity_topics(activity) assert Enum.member?(topics, "direct") diff --git a/test/pleroma/activity_test.exs b/test/pleroma/activity_test.exs index b5bb4bafe..67943d879 100644 --- a/test/pleroma/activity_test.exs +++ b/test/pleroma/activity_test.exs @@ -145,6 +145,7 @@ defmodule Pleroma.ActivityTest do setup do: clear_config([:instance, :limit_to_local_content]) + @tag :skip_darwin test "finds utf8 text in statuses", %{ japanese_activity: japanese_activity, user: user @@ -278,4 +279,78 @@ defmodule Pleroma.ActivityTest do assert Repo.aggregate(Activity, :count, :id) == 2 end + + describe "associated_object_id() sql function" do + test "with json object" do + %{rows: [[object_id]]} = + Ecto.Adapters.SQL.query!( + Pleroma.Repo, + """ + select associated_object_id('{"object": {"id":"foobar"}}'::jsonb); + """ + ) + + assert object_id == "foobar" + end + + test "with string object" do + %{rows: [[object_id]]} = + Ecto.Adapters.SQL.query!( + Pleroma.Repo, + """ + select associated_object_id('{"object": "foobar"}'::jsonb); + """ + ) + + assert object_id == "foobar" + end + + test "with array object" do + %{rows: [[object_id]]} = + Ecto.Adapters.SQL.query!( + Pleroma.Repo, + """ + select associated_object_id('{"object": ["foobar", {}]}'::jsonb); + """ + ) + + assert object_id == "foobar" + end + + test "invalid" do + %{rows: [[object_id]]} = + Ecto.Adapters.SQL.query!( + Pleroma.Repo, + """ + select associated_object_id('{"object": {}}'::jsonb); + """ + ) + + assert is_nil(object_id) + end + + test "invalid object id" do + %{rows: [[object_id]]} = + Ecto.Adapters.SQL.query!( + Pleroma.Repo, + """ + select associated_object_id('{"object": {"id": 123}}'::jsonb); + """ + ) + + assert is_nil(object_id) + end + + test "no object field" do + %{rows: [[object_id]]} = + Ecto.Adapters.SQL.query!( + Pleroma.Repo, + """ + select associated_object_id('{}'::jsonb); + """ + ) + + assert is_nil(object_id) + end + end end diff --git a/test/pleroma/bbs/handler_test.exs b/test/pleroma/bbs/handler_test.exs deleted file mode 100644 index aea3b6ead..000000000 --- a/test/pleroma/bbs/handler_test.exs +++ /dev/null @@ -1,89 +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.BBS.HandlerTest do - use Pleroma.DataCase, async: true - alias Pleroma.Activity - alias Pleroma.BBS.Handler - alias Pleroma.Object - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.CommonAPI - - import ExUnit.CaptureIO - import Pleroma.Factory - import Ecto.Query - - test "getting the home timeline" do - user = insert(:user) - followed = insert(:user) - - {:ok, user, followed} = User.follow(user, followed) - - {:ok, _first} = CommonAPI.post(user, %{status: "hey"}) - {:ok, _second} = CommonAPI.post(followed, %{status: "hello"}) - - output = - capture_io(fn -> - Handler.handle_command(%{user: user}, "home") - end) - - assert output =~ user.nickname - assert output =~ followed.nickname - - assert output =~ "hey" - assert output =~ "hello" - end - - test "posting" do - user = insert(:user) - - output = - capture_io(fn -> - Handler.handle_command(%{user: user}, "p this is a test post") - end) - - assert output =~ "Posted" - - activity = - Repo.one( - from(a in Activity, - where: fragment("?->>'type' = ?", a.data, "Create") - ) - ) - - assert activity.actor == user.ap_id - object = Object.normalize(activity, fetch: false) - assert object.data["content"] == "this is a test post" - end - - test "replying" do - user = insert(:user) - another_user = insert(:user) - - {:ok, activity} = CommonAPI.post(another_user, %{status: "this is a test post"}) - activity_object = Object.normalize(activity, fetch: false) - - output = - capture_io(fn -> - Handler.handle_command(%{user: user}, "r #{activity.id} this is a reply") - end) - - assert output =~ "Replied" - - reply = - Repo.one( - from(a in Activity, - where: fragment("?->>'type' = ?", a.data, "Create"), - where: a.actor == ^user.ap_id - ) - ) - - assert reply.actor == user.ap_id - - reply_object_data = Object.normalize(reply, fetch: false).data - assert reply_object_data["content"] == "this is a reply" - assert reply_object_data["inReplyTo"] == activity_object.data["id"] - end -end diff --git a/test/pleroma/bookmark_folder_test.exs b/test/pleroma/bookmark_folder_test.exs new file mode 100644 index 000000000..c45129b0e --- /dev/null +++ b/test/pleroma/bookmark_folder_test.exs @@ -0,0 +1,60 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.BookmarkFolderTest do + use Pleroma.DataCase, async: true + import Pleroma.Factory + alias Pleroma.BookmarkFolder + + describe "create/3" do + test "with valid params" do + user = insert(:user) + {:ok, folder} = BookmarkFolder.create(user.id, "Read later", "🕓") + assert folder.user_id == user.id + assert folder.name == "Read later" + assert folder.emoji == "🕓" + end + + test "with invalid params" do + {:error, changeset} = BookmarkFolder.create(nil, "", "not an emoji") + refute changeset.valid? + + assert changeset.errors == [ + emoji: {"Invalid emoji", []}, + user_id: {"can't be blank", [validation: :required]}, + name: {"can't be blank", [validation: :required]} + ] + end + end + + test "update/3" do + user = insert(:user) + {:ok, folder} = BookmarkFolder.create(user.id, "Read ltaer") + {:ok, folder} = BookmarkFolder.update(folder.id, "Read later") + assert folder.name == "Read later" + end + + test "for_user/1" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _} = BookmarkFolder.create(user.id, "Folder 1") + {:ok, _} = BookmarkFolder.create(user.id, "Folder 2") + {:ok, _} = BookmarkFolder.create(other_user.id, "Folder 3") + + folders = BookmarkFolder.for_user(user.id) + + assert length(folders) == 2 + end + + test "belongs_to_user?/2" do + user = insert(:user) + other_user = insert(:user) + + {:ok, folder} = BookmarkFolder.create(user.id, "Folder") + + assert true == BookmarkFolder.belongs_to_user?(folder.id, user.id) + assert false == BookmarkFolder.belongs_to_user?(folder.id, other_user.id) + end +end diff --git a/test/pleroma/bookmark_test.exs b/test/pleroma/bookmark_test.exs index 57144ded3..a2ed24c26 100644 --- a/test/pleroma/bookmark_test.exs +++ b/test/pleroma/bookmark_test.exs @@ -6,15 +6,17 @@ defmodule Pleroma.BookmarkTest do use Pleroma.DataCase, async: true import Pleroma.Factory alias Pleroma.Bookmark + alias Pleroma.BookmarkFolder alias Pleroma.Web.CommonAPI - describe "create/2" do + describe "create/3" do test "with valid params" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "Some cool information"}) {:ok, bookmark} = Bookmark.create(user.id, activity.id) assert bookmark.user_id == user.id assert bookmark.activity_id == activity.id + assert bookmark.folder_id == nil end test "with invalid params" do @@ -26,6 +28,19 @@ defmodule Pleroma.BookmarkTest do activity_id: {"can't be blank", [validation: :required]} ] end + + test "update existing bookmark folder" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "Some cool information"}) + + {:ok, bookmark} = Bookmark.create(user.id, activity.id) + assert bookmark.folder_id == nil + + {:ok, bookmark_folder} = BookmarkFolder.create(user.id, "Read later") + + {:ok, bookmark} = Bookmark.create(user.id, activity.id, bookmark_folder.id) + assert bookmark.folder_id == bookmark_folder.id + end end describe "destroy/2" do diff --git a/test/pleroma/config/deprecation_warnings_test.exs b/test/pleroma/config/deprecation_warnings_test.exs index f3453ddb0..fca2324ff 100644 --- a/test/pleroma/config/deprecation_warnings_test.exs +++ b/test/pleroma/config/deprecation_warnings_test.exs @@ -125,13 +125,12 @@ defmodule Pleroma.Config.DeprecationWarningsTest do media_removal: ["some.removal", {"some.other.instance", "Some reason"}] ) - expected_config = [ + expected_config = {:media_removal, [{"some.removal", ""}, {"some.other.instance", "Some reason"}]} - ] capture_log(fn -> DeprecationWarnings.warn() end) - assert Config.get([:mrf_simple]) == expected_config + assert expected_config in Config.get([:mrf_simple]) end test "doesn't give a warning with correct config" do @@ -215,7 +214,7 @@ defmodule Pleroma.Config.DeprecationWarningsTest do ``` config :pleroma, :mrf, - transparency_exclusions: [{"instance.tld", "Reason to exlude transparency"}] + transparency_exclusions: [{"instance.tld", "Reason to exclude transparency"}] ``` """ end @@ -327,11 +326,11 @@ defmodule Pleroma.Config.DeprecationWarningsTest do end) =~ "Your config is using old namespace for activity expiration configuration." end - test "check_uploders_s3_public_endpoint/0" do + test "check_uploaders_s3_public_endpoint/0" do clear_config([Pleroma.Uploaders.S3], public_endpoint: "https://fake.amazonaws.com/bucket/") assert capture_log(fn -> - DeprecationWarnings.check_uploders_s3_public_endpoint() + DeprecationWarnings.check_uploaders_s3_public_endpoint() end) =~ "Your config is using the old setting for controlling the URL of media uploaded to your S3 bucket." end diff --git a/test/pleroma/config/loader_test.exs b/test/pleroma/config/loader_test.exs index 095067e61..784817d49 100644 --- a/test/pleroma/config/loader_test.exs +++ b/test/pleroma/config/loader_test.exs @@ -11,7 +11,6 @@ defmodule Pleroma.Config.LoaderTest do config = Loader.read("test/fixtures/config/temp.secret.exs") assert config[:pleroma][:first_setting][:key] == "value" assert config[:pleroma][:first_setting][:key2] == [Pleroma.Repo] - assert config[:quack][:level] == :info end test "filter_group/2" do diff --git a/test/pleroma/config/release_runtime_provider_test.exs b/test/pleroma/config/release_runtime_provider_test.exs index 4e0d4c838..8d2a93d6c 100644 --- a/test/pleroma/config/release_runtime_provider_test.exs +++ b/test/pleroma/config/release_runtime_provider_test.exs @@ -10,13 +10,15 @@ defmodule Pleroma.Config.ReleaseRuntimeProviderTest do describe "load/2" do test "loads release defaults config and warns about non-existent runtime config" do ExUnit.CaptureIO.capture_io(fn -> - merged = ReleaseRuntimeProvider.load([], []) + merged = ReleaseRuntimeProvider.load([], config_path: "/var/empty/config.exs") assert merged == Pleroma.Config.Holder.release_defaults() end) =~ "!!! Config path is not declared! Please ensure it exists and that PLEROMA_CONFIG_PATH is unset or points to an existing file" end test "merged runtime config" do + assert :ok == File.chmod!("test/fixtures/config/temp.secret.exs", 0o640) + merged = ReleaseRuntimeProvider.load([], config_path: "test/fixtures/config/temp.secret.exs") @@ -25,6 +27,8 @@ defmodule Pleroma.Config.ReleaseRuntimeProviderTest do end test "merged exported config" do + assert :ok == File.chmod!("test/fixtures/config/temp.exported_from_db.secret.exs", 0o640) + ExUnit.CaptureIO.capture_io(fn -> merged = ReleaseRuntimeProvider.load([], @@ -37,6 +41,9 @@ defmodule Pleroma.Config.ReleaseRuntimeProviderTest do end test "runtime config is merged with exported config" do + assert :ok == File.chmod!("test/fixtures/config/temp.secret.exs", 0o640) + assert :ok == File.chmod!("test/fixtures/config/temp.exported_from_db.secret.exs", 0o640) + merged = ReleaseRuntimeProvider.load([], config_path: "test/fixtures/config/temp.secret.exs", diff --git a/test/pleroma/config/transfer_task_test.exs b/test/pleroma/config/transfer_task_test.exs index 927744add..6295fa888 100644 --- a/test/pleroma/config/transfer_task_test.exs +++ b/test/pleroma/config/transfer_task_test.exs @@ -15,13 +15,11 @@ defmodule Pleroma.Config.TransferTaskTest do test "transfer config values from db to env" do refute Application.get_env(:pleroma, :test_key) refute Application.get_env(:idna, :test_key) - refute Application.get_env(:quack, :test_key) refute Application.get_env(:postgrex, :test_key) initial = Application.get_env(:logger, :level) insert(:config, key: :test_key, value: [live: 2, com: 3]) insert(:config, group: :idna, key: :test_key, value: [live: 15, com: 35]) - insert(:config, group: :quack, key: :test_key, value: [:test_value1, :test_value2]) insert(:config, group: :postgrex, key: :test_key, value: :value) insert(:config, group: :logger, key: :level, value: :debug) @@ -29,36 +27,32 @@ defmodule Pleroma.Config.TransferTaskTest do assert Application.get_env(:pleroma, :test_key) == [live: 2, com: 3] assert Application.get_env(:idna, :test_key) == [live: 15, com: 35] - assert Application.get_env(:quack, :test_key) == [:test_value1, :test_value2] assert Application.get_env(:logger, :level) == :debug assert Application.get_env(:postgrex, :test_key) == :value on_exit(fn -> Application.delete_env(:pleroma, :test_key) Application.delete_env(:idna, :test_key) - Application.delete_env(:quack, :test_key) Application.delete_env(:postgrex, :test_key) Application.put_env(:logger, :level, initial) end) end test "transfer config values for 1 group and some keys" do - level = Application.get_env(:quack, :level) - meta = Application.get_env(:quack, :meta) + level = Application.get_env(:somegroup, :level) + meta = Application.get_env(:somegroup, :meta) - insert(:config, group: :quack, key: :level, value: :info) - insert(:config, group: :quack, key: :meta, value: [:none]) + insert(:config, group: :somegroup, key: :level, value: :info) + insert(:config, group: :somegroup, key: :meta, value: [:none]) TransferTask.start_link([]) - assert Application.get_env(:quack, :level) == :info - assert Application.get_env(:quack, :meta) == [:none] - default = Pleroma.Config.Holder.default_config(:quack, :webhook_url) - assert Application.get_env(:quack, :webhook_url) == default + assert Application.get_env(:somegroup, :level) == :info + assert Application.get_env(:somegroup, :meta) == [:none] on_exit(fn -> - Application.put_env(:quack, :level, level) - Application.put_env(:quack, :meta, meta) + Application.put_env(:somegroup, :level, level) + Application.put_env(:somegroup, :meta, meta) end) end @@ -79,35 +73,70 @@ defmodule Pleroma.Config.TransferTaskTest do describe "pleroma restart" do setup do - on_exit(fn -> Restarter.Pleroma.refresh() end) + on_exit(fn -> + Restarter.Pleroma.refresh() + + # Restarter.Pleroma.refresh/0 is an asynchronous call. + # A GenServer will first finish the previous call before starting a new one. + # Here we do a synchronous call. + # That way we are sure that the previous call has finished before we continue. + # See https://stackoverflow.com/questions/51361856/how-to-use-task-await-with-genserver + Restarter.Pleroma.rebooted?() + end) end - @tag :erratic test "don't restart if no reboot time settings were changed" do clear_config(:emoji) insert(:config, key: :emoji, value: [groups: [a: 1, b: 2]]) refute String.contains?( - capture_log(fn -> TransferTask.start_link([]) end), + capture_log(fn -> + TransferTask.start_link([]) + + # TransferTask.start_link/1 is an asynchronous call. + # A GenServer will first finish the previous call before starting a new one. + # Here we do a synchronous call. + # That way we are sure that the previous call has finished before we continue. + Restarter.Pleroma.rebooted?() + end), "pleroma restarted" ) end - @tag :erratic test "on reboot time key" do clear_config(:shout) insert(:config, key: :shout, value: [enabled: false]) - assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted" + + # Note that we don't actually restart Pleroma. + # See module Restarter.Pleroma + assert capture_log(fn -> + TransferTask.start_link([]) + + # TransferTask.start_link/1 is an asynchronous call. + # A GenServer will first finish the previous call before starting a new one. + # Here we do a synchronous call. + # That way we are sure that the previous call has finished before we continue. + Restarter.Pleroma.rebooted?() + end) =~ "pleroma restarted" end - @tag :erratic test "on reboot time subkey" do clear_config(Pleroma.Captcha) insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60]) - assert capture_log(fn -> TransferTask.start_link([]) end) =~ "pleroma restarted" + + # Note that we don't actually restart Pleroma. + # See module Restarter.Pleroma + assert capture_log(fn -> + TransferTask.start_link([]) + + # TransferTask.start_link/1 is an asynchronous call. + # A GenServer will first finish the previous call before starting a new one. + # Here we do a synchronous call. + # That way we are sure that the previous call has finished before we continue. + Restarter.Pleroma.rebooted?() + end) =~ "pleroma restarted" end - @tag :erratic test "don't restart pleroma on reboot time key and subkey if there is false flag" do clear_config(:shout) clear_config(Pleroma.Captcha) @@ -116,7 +145,15 @@ defmodule Pleroma.Config.TransferTaskTest do insert(:config, key: Pleroma.Captcha, value: [seconds_valid: 60]) refute String.contains?( - capture_log(fn -> TransferTask.load_and_update_env([], false) end), + capture_log(fn -> + TransferTask.load_and_update_env([], false) + + # TransferTask.start_link/1 is an asynchronous call. + # A GenServer will first finish the previous call before starting a new one. + # Here we do a synchronous call. + # That way we are sure that the previous call has finished before we continue. + Restarter.Pleroma.rebooted?() + end), "pleroma restarted" ) end diff --git a/test/pleroma/config_db_test.exs b/test/pleroma/config_db_test.exs index ba7c615e2..e20da1574 100644 --- a/test/pleroma/config_db_test.exs +++ b/test/pleroma/config_db_test.exs @@ -16,13 +16,13 @@ defmodule Pleroma.ConfigDBTest do test "get_all_as_keyword/0" do saved = insert(:config) - insert(:config, group: ":quack", key: ":level", value: :info) - insert(:config, group: ":quack", key: ":meta", value: [:none]) + insert(:config, group: ":goose", key: ":level", value: :info) + insert(:config, group: ":goose", key: ":meta", value: [:none]) insert(:config, - group: ":quack", + group: ":goose", key: ":webhook_url", - value: "https://hooks.slack.com/services/KEY/some_val" + value: "https://gander.com/" ) config = ConfigDB.get_all_as_keyword() @@ -31,9 +31,9 @@ defmodule Pleroma.ConfigDBTest do {saved.key, saved.value} ] - assert config[:quack][:level] == :info - assert config[:quack][:meta] == [:none] - assert config[:quack][:webhook_url] == "https://hooks.slack.com/services/KEY/some_val" + assert config[:goose][:level] == :info + assert config[:goose][:meta] == [:none] + assert config[:goose][:webhook_url] == "https://gander.com/" end describe "update_or_create/1" do @@ -267,10 +267,6 @@ defmodule Pleroma.ConfigDBTest do assert ConfigDB.to_elixir_types("ExSyslogger") == ExSyslogger end - test "Quack.Logger module" do - assert ConfigDB.to_elixir_types("Quack.Logger") == Quack.Logger - end - test "Swoosh.Adapters modules" do assert ConfigDB.to_elixir_types("Swoosh.Adapters.SMTP") == Swoosh.Adapters.SMTP assert ConfigDB.to_elixir_types("Swoosh.Adapters.AmazonSES") == Swoosh.Adapters.AmazonSES @@ -325,7 +321,7 @@ defmodule Pleroma.ConfigDBTest do }) == {:proxy_url, {:socks5, {127, 0, 0, 1}, 1234}} end - test "tuple with n childs" do + test "tuple with n children" do assert ConfigDB.to_elixir_types(%{ "tuple" => [ "v1", @@ -403,7 +399,7 @@ defmodule Pleroma.ConfigDBTest do assert ConfigDB.to_elixir_types(a: 1, b: 2, c: "string") == [a: 1, b: 2, c: "string"] end - test "complex keyword with nested mixed childs" do + test "complex keyword with nested mixed children" do assert ConfigDB.to_elixir_types([ %{"tuple" => [":uploader", "Pleroma.Uploaders.Local"]}, %{"tuple" => [":filters", ["Pleroma.Upload.Filter.Dedupe"]]}, @@ -447,13 +443,13 @@ defmodule Pleroma.ConfigDBTest do test "common keyword" do assert ConfigDB.to_elixir_types([ - %{"tuple" => [":level", ":warn"]}, + %{"tuple" => [":level", ":warning"]}, %{"tuple" => [":meta", [":all"]]}, %{"tuple" => [":path", ""]}, %{"tuple" => [":val", nil]}, %{"tuple" => [":webhook_url", "https://hooks.slack.com/services/YOUR-KEY-HERE"]} ]) == [ - level: :warn, + level: :warning, meta: [:all], path: "", val: nil, diff --git a/test/pleroma/conversation/participation_test.exs b/test/pleroma/conversation/participation_test.exs index 6f71cc040..697bdb7f9 100644 --- a/test/pleroma/conversation/participation_test.exs +++ b/test/pleroma/conversation/participation_test.exs @@ -57,7 +57,7 @@ defmodule Pleroma.Conversation.ParticipationTest do assert Participation.unread_count(other_user) == 0 end - test "for a new conversation, it sets the recipents of the participation" do + test "for a new conversation, it sets the recipients of the participation" do user = insert(:user) other_user = insert(:user) third_user = insert(:user) @@ -122,11 +122,11 @@ defmodule Pleroma.Conversation.ParticipationTest do end test "it marks a participation as read" do - participation = insert(:participation, %{read: false}) + participation = insert(:participation, %{updated_at: ~N[2017-07-17 17:09:58], read: false}) {:ok, updated_participation} = Participation.mark_as_read(participation) assert updated_participation.read - assert updated_participation.updated_at == participation.updated_at + assert :gt = NaiveDateTime.compare(updated_participation.updated_at, participation.updated_at) end test "it marks a participation as unread" do diff --git a/test/pleroma/conversation_test.exs b/test/pleroma/conversation_test.exs index 94897e7ea..809c1951a 100644 --- a/test/pleroma/conversation_test.exs +++ b/test/pleroma/conversation_test.exs @@ -13,6 +13,11 @@ defmodule Pleroma.ConversationTest do setup_all do: clear_config([:instance, :federating], true) + setup do + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + :ok + end + test "it goes through old direct conversations" do user = insert(:user) other_user = insert(:user) diff --git a/test/pleroma/docs/translator/compiler_test.exs b/test/pleroma/docs/translator/compiler_test.exs new file mode 100644 index 000000000..d6c3cdd40 --- /dev/null +++ b/test/pleroma/docs/translator/compiler_test.exs @@ -0,0 +1,90 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Docs.Translator.CompilerTest do + use ExUnit.Case, async: true + + alias Pleroma.Docs.Translator.Compiler + + @descriptions [ + %{ + key: "1", + label: "1", + description: "2", + children: [ + %{ + key: "3", + label: "3", + description: "4" + }, + %{ + key: "5", + label: "5", + description: "6" + } + ] + }, + %{ + key: "7", + label: "7", + description: "8", + children: [ + %{ + key: "9", + description: "9", + children: [ + %{ + key: "10", + description: "10", + children: [ + %{key: "11", description: "11"}, + %{description: "12"} + ] + } + ] + }, + %{ + label: "13" + } + ] + }, + %{ + group: "14", + label: "14" + }, + %{ + group: "15", + key: "15", + label: "15" + }, + %{ + group: {":subgroup", "16"}, + label: "16" + } + ] + + describe "extract_strings/1" do + test "it extracts all labels and descriptions" do + strings = Compiler.extract_strings(@descriptions) + assert length(strings) == 16 + + assert {["1"], "label", "1"} in strings + assert {["1"], "description", "2"} in strings + assert {["1", "3"], "label", "3"} in strings + assert {["1", "3"], "description", "4"} in strings + assert {["1", "5"], "label", "5"} in strings + assert {["1", "5"], "description", "6"} in strings + assert {["7"], "label", "7"} in strings + assert {["7"], "description", "8"} in strings + assert {["7", "9"], "description", "9"} in strings + assert {["7", "9", "10"], "description", "10"} in strings + assert {["7", "9", "10", "11"], "description", "11"} in strings + assert {["7", "9", "10", nil], "description", "12"} in strings + assert {["7", nil], "label", "13"} in strings + assert {["14"], "label", "14"} in strings + assert {["15-15"], "label", "15"} in strings + assert {["16"], "label", "16"} in strings + end + end +end diff --git a/test/pleroma/ecto_type/activity_pub/object_validators/bare_uri_test.exs b/test/pleroma/ecto_type/activity_pub/object_validators/bare_uri_test.exs new file mode 100644 index 000000000..760ecb465 --- /dev/null +++ b/test/pleroma/ecto_type/activity_pub/object_validators/bare_uri_test.exs @@ -0,0 +1,25 @@ +# 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.BareUriTest do + use Pleroma.DataCase, async: true + + alias Pleroma.EctoType.ActivityPub.ObjectValidators.BareUri + + test "diaspora://" do + text = "diaspora://alice@fediverse.example/post/deadbeefdeadbeefdeadbeefdeadbeef" + assert {:ok, ^text} = BareUri.cast(text) + end + + test "nostr:" do + text = "nostr:note1gwdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + assert {:ok, ^text} = BareUri.cast(text) + end + + test "errors for non-URIs" do + assert :error == BareUri.cast(1) + assert :error == BareUri.cast("foo") + assert :error == BareUri.cast("foo bar") + end +end diff --git a/test/pleroma/emoji/loader_test.exs b/test/pleroma/emoji/loader_test.exs index 717424fc8..22ee4e8d1 100644 --- a/test/pleroma/emoji/loader_test.exs +++ b/test/pleroma/emoji/loader_test.exs @@ -72,7 +72,7 @@ defmodule Pleroma.Emoji.LoaderTest do assert group == "special file" end - test "no mathing returns nil", %{groups: groups} do + test "no matching returns nil", %{groups: groups} do group = groups |> Loader.match_extra("/emoji/some_undefined.png") diff --git a/test/pleroma/emoji/pack_test.exs b/test/pleroma/emoji/pack_test.exs index 18b99da75..00001abfc 100644 --- a/test/pleroma/emoji/pack_test.exs +++ b/test/pleroma/emoji/pack_test.exs @@ -90,4 +90,8 @@ defmodule Pleroma.Emoji.PackTest do assert updated_pack.files_count == 1 end + + test "load_pack/1 ignores path traversal in a forged pack name", %{pack: pack} do + assert {:ok, ^pack} = Pack.load_pack("../../../../../dump_pack") + end end diff --git a/test/pleroma/emoji_test.exs b/test/pleroma/emoji_test.exs index 18063c223..85f4e8bbf 100644 --- a/test/pleroma/emoji_test.exs +++ b/test/pleroma/emoji_test.exs @@ -6,26 +6,26 @@ defmodule Pleroma.EmojiTest do use ExUnit.Case, async: true alias Pleroma.Emoji - describe "is_unicode_emoji?/1" do + describe "unicode?/1" do test "tells if a string is an unicode emoji" do - refute Emoji.is_unicode_emoji?("X") - refute Emoji.is_unicode_emoji?("ね") + refute Emoji.unicode?("X") + refute Emoji.unicode?("ね") # Only accept fully-qualified (RGI) emoji # See http://www.unicode.org/reports/tr51/ - refute Emoji.is_unicode_emoji?("❤") - refute Emoji.is_unicode_emoji?("☂") + refute Emoji.unicode?("❤") + refute Emoji.unicode?("☂") - assert Emoji.is_unicode_emoji?("🥺") - assert Emoji.is_unicode_emoji?("🤰") - assert Emoji.is_unicode_emoji?("❤️") - assert Emoji.is_unicode_emoji?("🏳️⚧️") - assert Emoji.is_unicode_emoji?("🫵") + assert Emoji.unicode?("🥺") + assert Emoji.unicode?("🤰") + assert Emoji.unicode?("❤️") + assert Emoji.unicode?("🏳️⚧️") + assert Emoji.unicode?("🫵") # Additionally, we accept regional indicators. - assert Emoji.is_unicode_emoji?("🇵") - assert Emoji.is_unicode_emoji?("🇴") - assert Emoji.is_unicode_emoji?("🇬") + assert Emoji.unicode?("🇵") + assert Emoji.unicode?("🇴") + assert Emoji.unicode?("🇬") end end diff --git a/test/pleroma/formatter_test.exs b/test/pleroma/formatter_test.exs index 5e431f6c9..46bb1db67 100644 --- a/test/pleroma/formatter_test.exs +++ b/test/pleroma/formatter_test.exs @@ -324,7 +324,7 @@ defmodule Pleroma.FormatterTest do assert {_text, [], ^expected_tags} = Formatter.linkify(text) end - test "parses mulitple tags in html" do + test "parses multiple tags in html" do text = "<p>#tag1 #tag2 #tag3 #tag4</p>" expected_tags = [ @@ -347,7 +347,7 @@ defmodule Pleroma.FormatterTest do assert {_text, [], ^expected_tags} = Formatter.linkify(text) end - test "parses mulitple tags on mulitple lines in html" do + test "parses multiple tags on multiple lines in html" do text = "<p>testing...</p><p>#tag1 #tag2 #tag3 #tag4</p><p>paragraph</p><p>#tag5 #tag6 #tag7 #tag8</p>" diff --git a/test/pleroma/healthcheck_test.exs b/test/pleroma/healthcheck_test.exs index dc540c9be..a8ab865ac 100644 --- a/test/pleroma/healthcheck_test.exs +++ b/test/pleroma/healthcheck_test.exs @@ -9,14 +9,16 @@ defmodule Pleroma.HealthcheckTest do test "system_info/0" do result = Healthcheck.system_info() |> Map.from_struct() - assert Map.keys(result) == [ + keys = Map.keys(result) + + assert Keyword.equal?(keys, [ :active, :healthy, :idle, :job_queue_stats, :memory_used, :pool_size - ] + ]) end describe "check_health/1" do @@ -25,7 +27,7 @@ defmodule Pleroma.HealthcheckTest do refute result.healthy end - test "chech_health/1" do + test "check_health/1" do result = Healthcheck.check_health(%Healthcheck{pool_size: 10, active: 9}) assert result.healthy end diff --git a/test/pleroma/html_test.exs b/test/pleroma/html_test.exs index 970baf63b..1be161971 100644 --- a/test/pleroma/html_test.exs +++ b/test/pleroma/html_test.exs @@ -17,6 +17,7 @@ defmodule Pleroma.HTMLTest do this is a link with allowed "rel" attribute: <a href="http://example.com/" rel="tag">example.com</a> this is a link with not allowed "rel" attribute: <a href="http://example.com/" rel="tag noallowed">example.com</a> this is an image: <img src="http://example.com/image.jpg"><br /> + this is an inline emoji: <img class="emoji" src="http://example.com/image.jpg"><br /> <script>alert('hacked')</script> """ @@ -24,6 +25,10 @@ defmodule Pleroma.HTMLTest do <img src="http://example.com/image.jpg" onerror="alert('hacked')"> """ + @html_stillimage_sample """ + <img class="still-image" src="http://example.com/image.jpg"> + """ + @html_span_class_sample """ <span class="animate-spin">hi</span> """ @@ -45,6 +50,7 @@ defmodule Pleroma.HTMLTest do this is a link with allowed "rel" attribute: example.com this is a link with not allowed "rel" attribute: example.com this is an image: + this is an inline emoji: alert('hacked') """ @@ -67,6 +73,7 @@ defmodule Pleroma.HTMLTest do this is a link with allowed "rel" attribute: <a href="http://example.com/" rel="tag">example.com</a> this is a link with not allowed "rel" attribute: <a href="http://example.com/">example.com</a> this is an image: <img src="http://example.com/image.jpg"/><br/> + this is an inline emoji: <img class="emoji" src="http://example.com/image.jpg"/><br/> alert('hacked') """ @@ -90,6 +97,15 @@ defmodule Pleroma.HTMLTest do HTML.filter_tags(@html_span_class_sample, Pleroma.HTML.Scrubber.TwitterText) end + test "does not allow images with invalid classes" do + expected = """ + <img src="http://example.com/image.jpg"/> + """ + + assert expected == + HTML.filter_tags(@html_stillimage_sample, Pleroma.HTML.Scrubber.TwitterText) + end + test "does allow microformats" do expected = """ <span class="h-card"><a class="u-url mention">@<span>foo</span></a></span> @@ -121,6 +137,7 @@ defmodule Pleroma.HTMLTest do this is a link with allowed "rel" attribute: <a href="http://example.com/" rel="tag">example.com</a> this is a link with not allowed "rel" attribute: <a href="http://example.com/">example.com</a> this is an image: <img src="http://example.com/image.jpg"/><br/> + this is an inline emoji: <img class="emoji" src="http://example.com/image.jpg"/><br/> alert('hacked') """ @@ -143,6 +160,15 @@ defmodule Pleroma.HTMLTest do assert expected == HTML.filter_tags(@html_span_class_sample, Pleroma.HTML.Scrubber.Default) end + test "does not allow images with invalid classes" do + expected = """ + <img src="http://example.com/image.jpg"/> + """ + + assert expected == + HTML.filter_tags(@html_stillimage_sample, Pleroma.HTML.Scrubber.TwitterText) + end + test "does allow microformats" do expected = """ <span class="h-card"><a class="u-url mention">@<span>foo</span></a></span> @@ -176,7 +202,7 @@ defmodule Pleroma.HTMLTest do }) object = Object.normalize(activity, fetch: false) - {:ok, url} = HTML.extract_first_external_url_from_object(object) + url = HTML.extract_first_external_url_from_object(object) assert url == "https://github.com/komeiji-satori/Dress" end @@ -191,7 +217,7 @@ defmodule Pleroma.HTMLTest do }) object = Object.normalize(activity, fetch: false) - {:ok, url} = HTML.extract_first_external_url_from_object(object) + url = HTML.extract_first_external_url_from_object(object) assert url == "https://github.com/syuilo/misskey/blob/develop/docs/setup.en.md" @@ -207,7 +233,7 @@ defmodule Pleroma.HTMLTest do }) object = Object.normalize(activity, fetch: false) - {:ok, url} = HTML.extract_first_external_url_from_object(object) + url = HTML.extract_first_external_url_from_object(object) assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140" end @@ -223,7 +249,7 @@ defmodule Pleroma.HTMLTest do }) object = Object.normalize(activity, fetch: false) - {:ok, url} = HTML.extract_first_external_url_from_object(object) + url = HTML.extract_first_external_url_from_object(object) assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140" end @@ -235,7 +261,7 @@ defmodule Pleroma.HTMLTest do object = Object.normalize(activity, fetch: false) - assert {:ok, nil} = HTML.extract_first_external_url_from_object(object) + assert nil == HTML.extract_first_external_url_from_object(object) end test "skips attachment links" do @@ -249,7 +275,7 @@ defmodule Pleroma.HTMLTest do object = Object.normalize(activity, fetch: false) - assert {:ok, nil} = HTML.extract_first_external_url_from_object(object) + assert nil == HTML.extract_first_external_url_from_object(object) end end end diff --git a/test/pleroma/http/adapter_helper/gun_test.exs b/test/pleroma/http/adapter_helper/gun_test.exs index 7515f4e79..d567bc844 100644 --- a/test/pleroma/http/adapter_helper/gun_test.exs +++ b/test/pleroma/http/adapter_helper/gun_test.exs @@ -36,7 +36,7 @@ defmodule Pleroma.HTTP.AdapterHelper.GunTest do assert opts[:certificates_verification] end - test "https url with non standart port" do + test "https url with non-standard port" do uri = URI.parse("https://example.com:115") opts = Gun.options([receive_conn: false], uri) @@ -44,7 +44,7 @@ defmodule Pleroma.HTTP.AdapterHelper.GunTest do assert opts[:certificates_verification] end - test "merges with defaul http adapter config" do + test "merges with default http adapter config" do defaults = Gun.options([receive_conn: false], URI.parse("https://example.com")) assert Keyword.has_key?(defaults, :a) assert Keyword.has_key?(defaults, :b) diff --git a/test/pleroma/http/adapter_helper/hackney_test.exs b/test/pleroma/http/adapter_helper/hackney_test.exs index 35d6c49a9..57ce4728c 100644 --- a/test/pleroma/http/adapter_helper/hackney_test.exs +++ b/test/pleroma/http/adapter_helper/hackney_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.HTTP.AdapterHelper.HackneyTest do - use ExUnit.Case, async: true + use ExUnit.Case use Pleroma.Tests.Helpers alias Pleroma.HTTP.AdapterHelper.Hackney diff --git a/test/pleroma/http/web_push_test.exs b/test/pleroma/http/web_push_test.exs new file mode 100644 index 000000000..dd8e45e6a --- /dev/null +++ b/test/pleroma/http/web_push_test.exs @@ -0,0 +1,45 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.HTTP.WebPushTest do + use ExUnit.Case + + import Tesla.Mock + alias Pleroma.HTTP + + @push_url "https://some-push-server/" + + setup do + mock(fn + %{ + method: :post, + url: @push_url, + headers: headers + } -> + if {"content-type", "octet-stream"} in headers do + %Tesla.Env{ + status: 200 + } + else + %Tesla.Env{ + status: 403 + } + end + end) + + :ok + end + + test "post" do + response = + HTTP.WebPush.post( + @push_url, + "encrypted payload", + %{"authorization" => "WebPush"}, + [] + ) + + assert {:ok, %{status: 200}} = response + end +end diff --git a/test/pleroma/instances/instance_test.exs b/test/pleroma/instances/instance_test.exs index 861519bce..6a718be21 100644 --- a/test/pleroma/instances/instance_test.exs +++ b/test/pleroma/instances/instance_test.exs @@ -31,14 +31,6 @@ defmodule Pleroma.Instances.InstanceTest do assert {:ok, instance} = Instance.set_reachable(instance.host) refute instance.unreachable_since end - - test "does NOT create an Instance record in case of no existing matching record" do - host = "domain.org" - assert nil == Instance.set_reachable(host) - - assert [] = Repo.all(Ecto.Query.from(i in Instance)) - assert Instance.reachable?(host) - end end describe "set_unreachable/1" do @@ -161,6 +153,66 @@ defmodule Pleroma.Instances.InstanceTest do end end + describe "get_or_update_metadata/1" do + test "Scrapes Wildebeest NodeInfo" do + Tesla.Mock.mock(fn + %{url: "https://wildebeest.example.org/.well-known/nodeinfo"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/wildebeest-well-known-nodeinfo.json") + } + + %{url: "https://wildebeest.example.org/nodeinfo/2.1"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/wildebeest-nodeinfo21.json") + } + end) + + expected = %{ + software_name: "wildebeest", + software_repository: "https://github.com/cloudflare/wildebeest", + software_version: "0.0.1" + } + + assert expected == + Instance.get_or_update_metadata(URI.parse("https://wildebeest.example.org/")) + + expected = %Pleroma.Instances.Instance.Pleroma.Instances.Metadata{ + software_name: "wildebeest", + software_repository: "https://github.com/cloudflare/wildebeest", + software_version: "0.0.1" + } + + assert expected == + Repo.get_by(Pleroma.Instances.Instance, %{host: "wildebeest.example.org"}).metadata + end + + test "Scrapes Mastodon NodeInfo" do + Tesla.Mock.mock(fn + %{url: "https://mastodon.example.org/.well-known/nodeinfo"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/mastodon-well-known-nodeinfo.json") + } + + %{url: "https://mastodon.example.org/nodeinfo/2.0"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/mastodon-nodeinfo20.json") + } + end) + + expected = %{ + software_name: "mastodon", + software_version: "4.1.0" + } + + assert expected == + Instance.get_or_update_metadata(URI.parse("https://mastodon.example.org/")) + end + end + test "delete_users_and_activities/1 deletes remote instance users and activities" do [mario, luigi, _peach, wario] = users = [ diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs index 2d4c7f63b..a0ffddf8d 100644 --- a/test/pleroma/integration/mastodon_websocket_test.exs +++ b/test/pleroma/integration/mastodon_websocket_test.exs @@ -31,22 +31,41 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do WebsocketClient.start_link(self(), path, headers) end + defp decode_json(json) do + with {:ok, %{"event" => event, "payload" => payload_text}} <- Jason.decode(json), + {:ok, payload} <- Jason.decode(payload_text) do + {:ok, %{"event" => event, "payload" => payload}} + end + end + + # Turns atom keys to strings + defp atom_key_to_string(json) do + json + |> Jason.encode!() + |> Jason.decode!() + end + test "refuses invalid requests" do capture_log(fn -> - assert {:error, {404, _}} = start_socket() - assert {:error, {404, _}} = start_socket("?stream=ncjdk") + assert {:error, %WebSockex.RequestError{code: 404}} = start_socket("?stream=ncjdk") Process.sleep(30) end) end test "requires authentication and a valid token for protected streams" do capture_log(fn -> - assert {:error, {401, _}} = start_socket("?stream=user&access_token=aaaaaaaaaaaa") - assert {:error, {401, _}} = start_socket("?stream=user") + assert {:error, %WebSockex.RequestError{code: 401}} = + start_socket("?stream=user&access_token=aaaaaaaaaaaa") + + assert {:error, %WebSockex.RequestError{code: 401}} = start_socket("?stream=user") Process.sleep(30) end) end + test "allows unified stream" do + assert {:ok, _} = start_socket() + end + test "allows public streams without authentication" do assert {:ok, _} = start_socket("?stream=public") assert {:ok, _} = start_socket("?stream=public:local") @@ -68,12 +87,143 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do view_json = Pleroma.Web.MastodonAPI.StatusView.render("show.json", activity: activity, for: nil) - |> Jason.encode!() - |> Jason.decode!() + |> atom_key_to_string() assert json == view_json end + describe "subscribing via WebSocket" do + test "can subscribe" do + user = insert(:user) + {:ok, pid} = start_socket() + WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!()) + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma:respond", + "payload" => %{"type" => "subscribe", "result" => "success"} + }} = decode_json(raw_json) + + {:ok, activity} = CommonAPI.post(user, %{status: "nice echo chamber"}) + + assert_receive {:text, raw_json}, 1_000 + assert {:ok, json} = Jason.decode(raw_json) + + assert "update" == json["event"] + 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!() + + assert json == view_json + end + + test "can subscribe to multiple streams" do + user = insert(:user) + {:ok, pid} = start_socket() + WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!()) + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma:respond", + "payload" => %{"type" => "subscribe", "result" => "success"} + }} = decode_json(raw_json) + + WebsocketClient.send_text( + pid, + %{type: "subscribe", stream: "hashtag", tag: "mew"} |> Jason.encode!() + ) + + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma:respond", + "payload" => %{"type" => "subscribe", "result" => "success"} + }} = decode_json(raw_json) + + {:ok, _activity} = CommonAPI.post(user, %{status: "nice echo chamber #mew"}) + + assert_receive {:text, raw_json}, 1_000 + assert {:ok, %{"stream" => stream1}} = Jason.decode(raw_json) + assert_receive {:text, raw_json}, 1_000 + assert {:ok, %{"stream" => stream2}} = Jason.decode(raw_json) + + streams = [stream1, stream2] + assert ["hashtag", "mew"] in streams + assert ["public"] in streams + end + + test "won't double subscribe" do + user = insert(:user) + {:ok, pid} = start_socket() + WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!()) + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma:respond", + "payload" => %{"type" => "subscribe", "result" => "success"} + }} = decode_json(raw_json) + + WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!()) + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma:respond", + "payload" => %{"type" => "subscribe", "result" => "ignored"} + }} = decode_json(raw_json) + + {:ok, _activity} = CommonAPI.post(user, %{status: "nice echo chamber"}) + + assert_receive {:text, _}, 1_000 + refute_receive {:text, _}, 1_000 + end + + test "rejects invalid streams" do + {:ok, pid} = start_socket() + WebsocketClient.send_text(pid, %{type: "subscribe", stream: "nonsense"} |> Jason.encode!()) + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma:respond", + "payload" => %{"type" => "subscribe", "result" => "error", "error" => "bad_topic"} + }} = decode_json(raw_json) + end + + test "can unsubscribe" do + user = insert(:user) + {:ok, pid} = start_socket() + WebsocketClient.send_text(pid, %{type: "subscribe", stream: "public"} |> Jason.encode!()) + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma:respond", + "payload" => %{"type" => "subscribe", "result" => "success"} + }} = decode_json(raw_json) + + WebsocketClient.send_text(pid, %{type: "unsubscribe", stream: "public"} |> Jason.encode!()) + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma:respond", + "payload" => %{"type" => "unsubscribe", "result" => "success"} + }} = decode_json(raw_json) + + {:ok, _activity} = CommonAPI.post(user, %{status: "nice echo chamber"}) + refute_receive {:text, _}, 1_000 + end + end + describe "with a valid user token" do setup do {:ok, app} = @@ -91,7 +241,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do {:ok, token} = OAuth.Token.exchange_token(app, auth) - %{user: user, token: token} + %{app: app, user: user, token: token} end test "accepts valid tokens", state do @@ -102,7 +252,7 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do assert {:ok, _} = start_socket("?stream=user&access_token=#{token.token}") capture_log(fn -> - assert {:error, {401, _}} = start_socket("?stream=user") + assert {:error, %WebSockex.RequestError{code: 401}} = start_socket("?stream=user") Process.sleep(30) end) end @@ -111,20 +261,225 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do assert {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}") capture_log(fn -> - assert {:error, {401, _}} = start_socket("?stream=user:notification") + assert {:error, %WebSockex.RequestError{code: 401}} = + start_socket("?stream=user:notification") + Process.sleep(30) 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}]) + test "accepts valid token on client-sent event", %{token: token} do + assert {:ok, pid} = start_socket() - capture_log(fn -> - assert {:error, {401, _}} = - start_socket("?stream=user", [{"Sec-WebSocket-Protocol", "I am a friend"}]) + WebsocketClient.send_text( + pid, + %{type: "pleroma:authenticate", token: token.token} |> Jason.encode!() + ) - Process.sleep(30) - end) + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma:respond", + "payload" => %{"type" => "pleroma:authenticate", "result" => "success"} + }} = decode_json(raw_json) + + WebsocketClient.send_text(pid, %{type: "subscribe", stream: "user"} |> Jason.encode!()) + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma:respond", + "payload" => %{"type" => "subscribe", "result" => "success"} + }} = decode_json(raw_json) + end + + test "rejects invalid token on client-sent event" do + assert {:ok, pid} = start_socket() + + WebsocketClient.send_text( + pid, + %{type: "pleroma:authenticate", token: "Something else"} |> Jason.encode!() + ) + + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma:respond", + "payload" => %{ + "type" => "pleroma:authenticate", + "result" => "error", + "error" => "unauthorized" + } + }} = decode_json(raw_json) + end + + test "rejects new authenticate request if already logged-in", %{token: token} do + assert {:ok, pid} = start_socket() + + WebsocketClient.send_text( + pid, + %{type: "pleroma:authenticate", token: token.token} |> Jason.encode!() + ) + + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma:respond", + "payload" => %{"type" => "pleroma:authenticate", "result" => "success"} + }} = decode_json(raw_json) + + WebsocketClient.send_text( + pid, + %{type: "pleroma:authenticate", token: "Something else"} |> Jason.encode!() + ) + + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma:respond", + "payload" => %{ + "type" => "pleroma:authenticate", + "result" => "error", + "error" => "already_authenticated" + } + }} = decode_json(raw_json) + end + + test "accepts the 'list' stream", %{token: token, user: user} do + posting_user = insert(:user) + + {:ok, list} = Pleroma.List.create("test", user) + Pleroma.List.follow(list, posting_user) + + assert {:ok, _} = start_socket("?stream=list&access_token=#{token.token}&list=#{list.id}") + + assert {:ok, pid} = start_socket("?access_token=#{token.token}") + + WebsocketClient.send_text( + pid, + %{type: "subscribe", stream: "list", list: list.id} |> Jason.encode!() + ) + + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma:respond", + "payload" => %{"type" => "subscribe", "result" => "success"} + }} = decode_json(raw_json) + + WebsocketClient.send_text( + pid, + %{type: "subscribe", stream: "list", list: to_string(list.id)} |> Jason.encode!() + ) + + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "pleroma:respond", + "payload" => %{"type" => "subscribe", "result" => "ignored"} + }} = decode_json(raw_json) + end + + test "disconnect when token is revoked", %{app: app, user: user, token: token} do + assert {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}") + assert {:ok, _} = start_socket("?stream=user&access_token=#{token.token}") + + {:ok, auth} = OAuth.Authorization.create_authorization(app, user) + + {:ok, token2} = OAuth.Token.exchange_token(app, auth) + assert {:ok, _} = start_socket("?stream=user&access_token=#{token2.token}") + + OAuth.Token.Strategy.Revoke.revoke(token) + + assert_receive {:close, _} + assert_receive {:close, _} + refute_receive {:close, _} + end + + test "receives private statuses", %{user: reading_user, token: token} do + user = insert(:user) + CommonAPI.follow(reading_user, user) + + {:ok, _} = start_socket("?stream=user&access_token=#{token.token}") + + {:ok, activity} = + CommonAPI.post(user, %{status: "nice echo chamber", visibility: "private"}) + + assert_receive {:text, raw_json}, 1_000 + assert {:ok, json} = Jason.decode(raw_json) + + assert "update" == json["event"] + assert json["payload"] + assert {:ok, json} = Jason.decode(json["payload"]) + + view_json = + Pleroma.Web.MastodonAPI.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) + + {:ok, _} = start_socket("?stream=user&access_token=#{token.token}") + + {:ok, activity} = + CommonAPI.post(user, %{status: "nice echo chamber", visibility: "private"}) + + assert_receive {:text, _raw_json}, 1_000 + + {:ok, _} = CommonAPI.update(user, activity, %{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 + ) + |> 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) + + {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}") + + {:ok, %Pleroma.Activity{id: activity_id} = _activity} = + CommonAPI.post(user, %{ + status: "nice echo chamber @#{reading_user.nickname}", + visibility: "private" + }) + + assert_receive {:text, raw_json}, 1_000 + + assert {:ok, + %{ + "event" => "notification", + "payload" => %{ + "status" => %{ + "id" => ^activity_id + } + } + }} = decode_json(raw_json) end end end diff --git a/test/pleroma/maps_test.exs b/test/pleroma/maps_test.exs new file mode 100644 index 000000000..05f1b18b2 --- /dev/null +++ b/test/pleroma/maps_test.exs @@ -0,0 +1,22 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2024 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.MapsTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Maps + + describe "filter_empty_values/1" do + assert %{"bar" => "b", "ray" => ["foo"], "objs" => %{"a" => "b"}} == + Maps.filter_empty_values(%{ + "foo" => nil, + "fooz" => "", + "bar" => "b", + "rei" => [], + "ray" => ["foo"], + "obj" => %{}, + "objs" => %{"a" => "b"} + }) + end +end diff --git a/test/pleroma/mfa/totp_test.exs b/test/pleroma/mfa/totp_test.exs index 56e4f48ed..f291ed14b 100644 --- a/test/pleroma/mfa/totp_test.exs +++ b/test/pleroma/mfa/totp_test.exs @@ -7,6 +7,8 @@ defmodule Pleroma.MFA.TOTPTest do alias Pleroma.MFA.TOTP + import Pleroma.Tests.Helpers, only: [uri_equal?: 2] + test "create provisioning_uri to generate qrcode" do uri = TOTP.provisioning_uri("test-secrcet", "test@example.com", @@ -15,7 +17,9 @@ defmodule Pleroma.MFA.TOTPTest do period: 60 ) - assert uri == + assert uri_equal?( + uri, "otpauth://totp/test@example.com?digits=8&issuer=Plerome-42&period=60&secret=test-secrcet" + ) end end diff --git a/test/pleroma/notification_test.exs b/test/pleroma/notification_test.exs index 805764ea4..2c582c708 100644 --- a/test/pleroma/notification_test.exs +++ b/test/pleroma/notification_test.exs @@ -3,10 +3,9 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.NotificationTest do - use Pleroma.DataCase + use Pleroma.DataCase, async: false import Pleroma.Factory - import Mock alias Pleroma.FollowingRelationship alias Pleroma.Notification @@ -18,8 +17,11 @@ defmodule Pleroma.NotificationTest do alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.NotificationView - alias Pleroma.Web.Push - alias Pleroma.Web.Streamer + + setup do + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + :ok + end describe "create_notifications" do test "never returns nil" do @@ -32,20 +34,26 @@ defmodule Pleroma.NotificationTest do refute {:ok, [nil]} == Notification.create_notifications(activity) end - test "creates a notification for a report" do + test "creates a report notification only for privileged users" do reporting_user = insert(:user) reported_user = insert(:user) - {:ok, moderator_user} = insert(:user) |> User.admin_api_update(%{is_moderator: true}) + moderator_user = insert(:user, is_moderator: true) - {:ok, activity} = CommonAPI.report(reporting_user, %{account_id: reported_user.id}) + clear_config([:instance, :moderator_privileges], []) + {:ok, activity1} = CommonAPI.report(reporting_user, %{account_id: reported_user.id}) + {:ok, []} = Notification.create_notifications(activity1) - {:ok, [notification]} = Notification.create_notifications(activity) + clear_config([:instance, :moderator_privileges], [:reports_manage_reports]) + {:ok, activity2} = CommonAPI.report(reporting_user, %{account_id: reported_user.id}) + {:ok, [notification]} = Notification.create_notifications(activity2) assert notification.user_id == moderator_user.id assert notification.type == "pleroma:report" end - test "suppresses notification to reporter if reporter is an admin" do + test "suppresses notifications for own reports" do + clear_config([:instance, :admin_privileges], [:reports_manage_reports]) + reporting_admin = insert(:user, is_admin: true) reported_user = insert(:user) other_admin = insert(:user, is_admin: true) @@ -104,6 +112,7 @@ defmodule Pleroma.NotificationTest do {:ok, [notification]} = Notification.create_notifications(status) assert notification.user_id == subscriber.id + assert notification.type == "status" end test "does not create a notification for subscribed users if status is a reply" do @@ -127,174 +136,72 @@ defmodule Pleroma.NotificationTest do subscriber_notifications = Notification.for_user(subscriber) assert Enum.empty?(subscriber_notifications) end - end - - test "create_poll_notifications/1" do - [user1, user2, user3, _, _] = insert_list(5, :user) - question = insert(:question, user: user1) - activity = insert(:question_activity, question: question) - {:ok, _, _} = CommonAPI.vote(user2, question, [0]) - {:ok, _, _} = CommonAPI.vote(user3, question, [1]) - - {:ok, notifications} = Notification.create_poll_notifications(activity) - - assert [user2.id, user3.id, user1.id] == Enum.map(notifications, & &1.user_id) - end - - describe "CommonApi.post/2 notification-related functionality" do - test_with_mock "creates but does NOT send notification to blocker user", - Push, - [:passthrough], - [] do + test "does not create subscriber notification if mentioned" do user = insert(:user) - blocker = insert(:user) - {:ok, _user_relationship} = User.block(blocker, user) - - {:ok, _activity} = CommonAPI.post(user, %{status: "hey @#{blocker.nickname}!"}) + subscriber = insert(:user) - blocker_id = blocker.id - assert [%Notification{user_id: ^blocker_id}] = Repo.all(Notification) - refute called(Push.send(:_)) - end + User.subscribe(subscriber, user) - test_with_mock "creates but does NOT send notification to notification-muter user", - Push, - [:passthrough], - [] do - user = insert(:user) - muter = insert(:user) - {:ok, _user_relationships} = User.mute(muter, user) + {:ok, status} = CommonAPI.post(user, %{status: "mentioning @#{subscriber.nickname}"}) + {:ok, [notification] = notifications} = Notification.create_notifications(status) - {:ok, _activity} = CommonAPI.post(user, %{status: "hey @#{muter.nickname}!"}) + assert length(notifications) == 1 - muter_id = muter.id - assert [%Notification{user_id: ^muter_id}] = Repo.all(Notification) - refute called(Push.send(:_)) + assert notification.user_id == subscriber.id + assert notification.type == "mention" end - test_with_mock "creates but does NOT send notification to thread-muter user", - Push, - [:passthrough], - [] do + test "it sends edited notifications to those who repeated a status" do user = insert(:user) - thread_muter = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{thread_muter.nickname}!"}) - - {:ok, _} = CommonAPI.add_mute(thread_muter, activity) + repeated_user = insert(:user) + other_user = insert(:user) - {:ok, _same_context_activity} = + {:ok, activity_one} = CommonAPI.post(user, %{ - status: "hey-hey-hey @#{thread_muter.nickname}!", - in_reply_to_status_id: activity.id + status: "hey @#{other_user.nickname}!" }) - [pre_mute_notification, post_mute_notification] = - Repo.all(from(n in Notification, where: n.user_id == ^thread_muter.id, order_by: n.id)) + {:ok, _activity_two} = CommonAPI.repeat(activity_one.id, repeated_user) - pre_mute_notification_id = pre_mute_notification.id - post_mute_notification_id = post_mute_notification.id + {:ok, _edit_activity} = + CommonAPI.update(user, activity_one, %{ + status: "hey @#{other_user.nickname}! mew mew" + }) - assert called( - Push.send( - :meck.is(fn - %Notification{id: ^pre_mute_notification_id} -> true - _ -> false - end) - ) - ) - - refute called( - Push.send( - :meck.is(fn - %Notification{id: ^post_mute_notification_id} -> true - _ -> false - end) - ) - ) + assert [%{type: "reblog"}] = Notification.for_user(user) + assert [%{type: "update"}] = Notification.for_user(repeated_user) + assert [%{type: "mention"}] = Notification.for_user(other_user) end end - describe "create_notification" do - @tag needs_streamer: true - test "it creates a notification for user and send to the 'user' and the 'user:notification' stream" do - %{user: user, token: oauth_token} = oauth_access(["read"]) - - task = - Task.async(fn -> - {:ok, _topic} = Streamer.get_topic_and_add_socket("user", user, oauth_token) - assert_receive {:render_with_user, _, _, _}, 4_000 - end) - - task_user_notification = - Task.async(fn -> - {:ok, _topic} = - Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) - - assert_receive {:render_with_user, _, _, _}, 4_000 - end) - - activity = insert(:note_activity) - - notify = Notification.create_notification(activity, user) - assert notify.user_id == user.id - Task.await(task) - Task.await(task_user_notification) - end - - test "it creates a notification for user if the user blocks the activity author" do - activity = insert(:note_activity) - author = User.get_cached_by_ap_id(activity.data["actor"]) - user = insert(:user) - {:ok, _user_relationship} = User.block(user, author) - - assert Notification.create_notification(activity, user) - end - - test "it creates a notification for the user if the user mutes the activity author" do - muter = insert(:user) - muted = insert(:user) - {:ok, _} = User.mute(muter, muted) - muter = Repo.get(User, muter.id) - {:ok, activity} = CommonAPI.post(muted, %{status: "Hi @#{muter.nickname}"}) - - notification = Notification.create_notification(activity, muter) - - assert notification.id - assert notification.seen - end - - test "notification created if user is muted without notifications" do - muter = insert(:user) - muted = insert(:user) - - {:ok, _user_relationships} = User.mute(muter, muted, %{notifications: false}) + test "create_poll_notifications/1" do + [user1, user2, user3, _, _] = insert_list(5, :user) + question = insert(:question, user: user1) + activity = insert(:question_activity, question: question) - {:ok, activity} = CommonAPI.post(muted, %{status: "Hi @#{muter.nickname}"}) + {:ok, _, _} = CommonAPI.vote(user2, question, [0]) + {:ok, _, _} = CommonAPI.vote(user3, question, [1]) - assert Notification.create_notification(activity, muter) - end + {:ok, notifications} = Notification.create_poll_notifications(activity) - test "it creates a notification for an activity from a muted thread" do - muter = insert(:user) - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(muter, %{status: "hey"}) - CommonAPI.add_mute(muter, activity) + assert [user2.id, user3.id, user1.id] == Enum.map(notifications, & &1.user_id) + end - {:ok, activity} = - CommonAPI.post(other_user, %{ - status: "Hi @#{muter.nickname}", - in_reply_to_status_id: activity.id - }) + describe "create_notification" do + test "it disables notifications from strangers" do + follower = insert(:user) - notification = Notification.create_notification(activity, muter) + followed = + insert(:user, + notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true} + ) - assert notification.id - assert notification.seen + {:ok, activity} = CommonAPI.post(follower, %{status: "hey @#{followed.nickname}"}) + refute Notification.create_notification(activity, followed) end - test "it disables notifications from strangers" do + test "it disables notifications from non-followees" do follower = insert(:user) followed = @@ -302,10 +209,24 @@ defmodule Pleroma.NotificationTest do notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true} ) + CommonAPI.follow(follower, followed) {:ok, activity} = CommonAPI.post(follower, %{status: "hey @#{followed.nickname}"}) refute Notification.create_notification(activity, followed) end + test "it allows notifications from followees" do + poster = insert(:user) + + receiver = + insert(:user, + notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true} + ) + + CommonAPI.follow(receiver, poster) + {:ok, activity} = CommonAPI.post(poster, %{status: "hey @#{receiver.nickname}"}) + assert Notification.create_notification(activity, receiver) + end + test "it doesn't create a notification for user if he is the activity author" do activity = insert(:note_activity) author = User.get_cached_by_ap_id(activity.data["actor"]) @@ -520,25 +441,6 @@ defmodule Pleroma.NotificationTest do end end - describe "destroy_multiple_from_types/2" do - test "clears all notifications of a certain type for a given user" do - report_activity = insert(:report_activity) - user1 = insert(:user, is_moderator: true, is_admin: true) - user2 = insert(:user, is_moderator: true, is_admin: true) - {:ok, _} = Notification.create_notifications(report_activity) - - {:ok, _} = - CommonAPI.post(user2, %{ - status: "hey @#{user1.nickname} !" - }) - - Notification.destroy_multiple_from_types(user1, ["pleroma:report"]) - - assert [%Pleroma.Notification{type: "mention"}] = Notification.for_user(user1) - assert [%Pleroma.Notification{type: "pleroma:report"}] = Notification.for_user(user2) - end - end - describe "set_read_up_to()" do test "it sets all notifications as read up to a specified notification ID" do user = insert(:user) @@ -563,9 +465,7 @@ defmodule Pleroma.NotificationTest do status: "hey yet again @#{other_user.nickname}!" }) - [_, read_notification] = Notification.set_read_up_to(other_user, n2.id) - - assert read_notification.activity.object + Notification.set_read_up_to(other_user, n2.id) [n3, n2, n1] = Notification.for_user(other_user) @@ -640,7 +540,7 @@ defmodule Pleroma.NotificationTest do status: "hey @#{other_user.nickname}!" }) - {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity) + enabled_receivers = Notification.get_notified_from_activity(activity) assert other_user in enabled_receivers end @@ -672,7 +572,7 @@ defmodule Pleroma.NotificationTest do {:ok, activity} = Transmogrifier.handle_incoming(create_activity) - {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity) + enabled_receivers = Notification.get_notified_from_activity(activity) assert other_user in enabled_receivers end @@ -699,7 +599,7 @@ defmodule Pleroma.NotificationTest do {:ok, activity} = Transmogrifier.handle_incoming(create_activity) - {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity) + enabled_receivers = Notification.get_notified_from_activity(activity) assert other_user not in enabled_receivers end @@ -716,8 +616,7 @@ defmodule Pleroma.NotificationTest do {:ok, activity_two} = CommonAPI.favorite(third_user, activity_one.id) - {enabled_receivers, _disabled_receivers} = - Notification.get_notified_from_activity(activity_two) + enabled_receivers = Notification.get_notified_from_activity(activity_two) assert other_user not in enabled_receivers end @@ -739,7 +638,7 @@ defmodule Pleroma.NotificationTest do |> Map.put("to", [other_user.ap_id | like_data["to"]]) |> ActivityPub.persist(local: true) - {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(like) + enabled_receivers = Notification.get_notified_from_activity(like) assert other_user not in enabled_receivers end @@ -756,39 +655,36 @@ defmodule Pleroma.NotificationTest do {:ok, activity_two} = CommonAPI.repeat(activity_one.id, third_user) - {enabled_receivers, _disabled_receivers} = - Notification.get_notified_from_activity(activity_two) + enabled_receivers = Notification.get_notified_from_activity(activity_two) assert other_user not in enabled_receivers end - test "it returns blocking recipient in disabled recipients list" do + test "it does not return blocking recipient in recipients list" do user = insert(:user) other_user = insert(:user) {:ok, _user_relationship} = User.block(other_user, user) {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) - {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity) + enabled_receivers = Notification.get_notified_from_activity(activity) assert [] == enabled_receivers - assert [other_user] == disabled_receivers end - test "it returns notification-muting recipient in disabled recipients list" do + test "it does not return notification-muting recipient in recipients list" do user = insert(:user) other_user = insert(:user) {:ok, _user_relationships} = User.mute(other_user, user) {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) - {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity) + enabled_receivers = Notification.get_notified_from_activity(activity) assert [] == enabled_receivers - assert [other_user] == disabled_receivers end - test "it returns thread-muting recipient in disabled recipients list" do + test "it does not return thread-muting recipient in recipients list" do user = insert(:user) other_user = insert(:user) @@ -802,14 +698,12 @@ defmodule Pleroma.NotificationTest do in_reply_to_status_id: activity.id }) - {enabled_receivers, disabled_receivers} = - Notification.get_notified_from_activity(same_context_activity) + enabled_receivers = Notification.get_notified_from_activity(same_context_activity) - assert [other_user] == disabled_receivers refute other_user in enabled_receivers end - test "it returns non-following domain-blocking recipient in disabled recipients list" do + test "it does not return non-following domain-blocking recipient in recipients list" do blocked_domain = "blocked.domain" user = insert(:user, %{ap_id: "https://#{blocked_domain}/@actor"}) other_user = insert(:user) @@ -818,10 +712,9 @@ defmodule Pleroma.NotificationTest do {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) - {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity) + enabled_receivers = Notification.get_notified_from_activity(activity) assert [] == enabled_receivers - assert [other_user] == disabled_receivers end test "it returns following domain-blocking recipient in enabled recipients list" do @@ -834,10 +727,32 @@ defmodule Pleroma.NotificationTest do {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"}) - {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity) + enabled_receivers = Notification.get_notified_from_activity(activity) assert [other_user] == enabled_receivers - assert [] == disabled_receivers + end + + test "it sends edited notifications to those who repeated a status" do + user = insert(:user) + repeated_user = insert(:user) + other_user = insert(:user) + + {:ok, activity_one} = + CommonAPI.post(user, %{ + status: "hey @#{other_user.nickname}!" + }) + + {:ok, _activity_two} = CommonAPI.repeat(activity_one.id, repeated_user) + + {:ok, edit_activity} = + CommonAPI.update(user, activity_one, %{ + status: "hey @#{other_user.nickname}! mew mew" + }) + + enabled_receivers = Notification.get_notified_from_activity(edit_activity) + + assert repeated_user in enabled_receivers + refute other_user in enabled_receivers end end @@ -944,22 +859,6 @@ defmodule Pleroma.NotificationTest do assert Enum.empty?(Notification.for_user(user)) end - test "replying to a deleted post without tagging does not generate a notification" do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity} = CommonAPI.post(user, %{status: "test post"}) - {:ok, _deletion_activity} = CommonAPI.delete(activity.id, user) - - {:ok, _reply_activity} = - CommonAPI.post(other_user, %{ - status: "test reply", - in_reply_to_status_id: activity.id - }) - - assert Enum.empty?(Notification.for_user(user)) - end - test "notifications are deleted if a local user is deleted" do user = insert(:user) other_user = insert(:user) @@ -1125,13 +1024,13 @@ defmodule Pleroma.NotificationTest do assert Notification.for_user(user) == [] end - test "it returns notifications from a muted user when with_muted is set", %{user: user} do + test "it doesn't return notifications from a muted user when with_muted is set", %{user: user} do muted = insert(:user) {:ok, _user_relationships} = User.mute(user, muted) {:ok, _activity} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"}) - assert length(Notification.for_user(user, %{with_muted: true})) == 1 + assert Enum.empty?(Notification.for_user(user, %{with_muted: true})) end test "it doesn't return notifications from a blocked user when with_muted is set", %{ @@ -1192,5 +1091,32 @@ defmodule Pleroma.NotificationTest do assert length(Notification.for_user(user)) == 1 end + + test "it returns notifications when related object is without content and filters are defined", + %{user: user} do + followed_user = insert(:user, is_locked: true) + + insert(:filter, user: followed_user, phrase: "test", hide: true) + + {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user) + refute FollowingRelationship.following?(user, followed_user) + assert [notification] = Notification.for_user(followed_user) + + assert %{type: "follow_request"} = + NotificationView.render("show.json", %{ + notification: notification, + for: followed_user + }) + + assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user) + + assert [notification] = Notification.for_user(followed_user) + + assert %{type: "follow"} = + NotificationView.render("show.json", %{ + notification: notification, + for: followed_user + }) + end end end diff --git a/test/pleroma/object/fetcher_test.exs b/test/pleroma/object/fetcher_test.exs index 98130f434..6f21452a7 100644 --- a/test/pleroma/object/fetcher_test.exs +++ b/test/pleroma/object/fetcher_test.exs @@ -6,10 +6,15 @@ 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 + + require Pleroma.Constants import Mock + import Pleroma.Factory import Tesla.Mock setup do @@ -96,8 +101,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, "Max thread distance exceeded."} = - Fetcher.fetch_object_from_id(@ap_id, depth: 1) + assert {:error, :allowed_depth} = 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 @@ -159,6 +163,17 @@ defmodule Pleroma.Object.FetcherTest do "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) + + {:ok, _object} = + Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") + + assert Instances.reachable?(id) + end end describe "implementation quirks" do @@ -204,14 +219,14 @@ defmodule Pleroma.Object.FetcherTest do end test "handle HTTP 410 Gone response" do - assert {:error, "Object has been deleted"} == + assert {:error, :not_found} == Fetcher.fetch_and_contain_remote_object_from_id( "https://mastodon.example.org/users/userisgone" ) end test "handle HTTP 404 response" do - assert {:error, "Object has been deleted"} == + assert {:error, :not_found} == Fetcher.fetch_and_contain_remote_object_from_id( "https://mastodon.example.org/users/userisgone404" ) @@ -269,4 +284,331 @@ defmodule Pleroma.Object.FetcherTest do refute called(Pleroma.Signature.sign(:_, :_)) end end + + describe "refetching" do + setup do + insert(:user, ap_id: "https://mastodon.social/users/emelie") + + object1 = %{ + "id" => "https://mastodon.social/1", + "actor" => "https://mastodon.social/users/emelie", + "attributedTo" => "https://mastodon.social/users/emelie", + "type" => "Note", + "content" => "test 1", + "bcc" => [], + "bto" => [], + "cc" => [], + "to" => [Pleroma.Constants.as_public()], + "summary" => "", + "published" => "2023-05-08 23:43:20Z", + "updated" => "2023-05-09 23:43:20Z" + } + + {:ok, local_object1, _} = ObjectValidator.validate(object1, []) + + object2 = %{ + "id" => "https://mastodon.social/2", + "actor" => "https://mastodon.social/users/emelie", + "attributedTo" => "https://mastodon.social/users/emelie", + "type" => "Note", + "content" => "test 2", + "bcc" => [], + "bto" => [], + "cc" => [], + "to" => [Pleroma.Constants.as_public()], + "summary" => "", + "published" => "2023-05-08 23:43:20Z", + "updated" => "2023-05-09 23:43:25Z", + "formerRepresentations" => %{ + "type" => "OrderedCollection", + "orderedItems" => [ + %{ + "type" => "Note", + "content" => "orig 2", + "actor" => "https://mastodon.social/users/emelie", + "attributedTo" => "https://mastodon.social/users/emelie", + "bcc" => [], + "bto" => [], + "cc" => [], + "to" => [Pleroma.Constants.as_public()], + "summary" => "", + "published" => "2023-05-08 23:43:20Z", + "updated" => "2023-05-09 23:43:21Z" + } + ], + "totalItems" => 1 + } + } + + {:ok, local_object2, _} = ObjectValidator.validate(object2, []) + + mock(fn + %{ + method: :get, + url: "https://mastodon.social/1" + } -> + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/activity+json"}], + body: Jason.encode!(object1 |> Map.put("updated", "2023-05-09 23:44:20Z")) + } + + %{ + method: :get, + url: "https://mastodon.social/2" + } -> + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/activity+json"}], + body: Jason.encode!(object2 |> Map.put("updated", "2023-05-09 23:44:20Z")) + } + + %{ + method: :get, + url: "https://mastodon.social/users/emelie/collections/featured" + } -> + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/activity+json"}], + body: + Jason.encode!(%{ + "id" => "https://mastodon.social/users/emelie/collections/featured", + "type" => "OrderedCollection", + "actor" => "https://mastodon.social/users/emelie", + "attributedTo" => "https://mastodon.social/users/emelie", + "orderedItems" => [], + "totalItems" => 0 + }) + } + + env -> + apply(HttpRequestMock, :request, [env]) + end) + + %{object1: local_object1, object2: local_object2} + end + + test "it keeps formerRepresentations if remote does not have this attr", %{object1: object1} do + full_object1 = + object1 + |> Map.merge(%{ + "formerRepresentations" => %{ + "type" => "OrderedCollection", + "orderedItems" => [ + %{ + "type" => "Note", + "content" => "orig 2", + "actor" => "https://mastodon.social/users/emelie", + "attributedTo" => "https://mastodon.social/users/emelie", + "bcc" => [], + "bto" => [], + "cc" => [], + "to" => [Pleroma.Constants.as_public()], + "summary" => "", + "published" => "2023-05-08 23:43:20Z" + } + ], + "totalItems" => 1 + } + }) + + {:ok, o} = Object.create(full_object1) + + assert {:ok, refetched} = Fetcher.refetch_object(o) + + assert %{"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}} = + refetched.data + end + + test "it uses formerRepresentations from remote if possible", %{object2: object2} do + {:ok, o} = Object.create(object2) + + assert {:ok, refetched} = Fetcher.refetch_object(o) + + assert %{"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}} = + refetched.data + end + + test "it replaces formerRepresentations with the one from remote", %{object2: object2} do + full_object2 = + object2 + |> Map.merge(%{ + "content" => "mew mew #def", + "formerRepresentations" => %{ + "type" => "OrderedCollection", + "orderedItems" => [ + %{"type" => "Note", "content" => "mew mew 2"} + ], + "totalItems" => 1 + } + }) + + {:ok, o} = Object.create(full_object2) + + assert {:ok, refetched} = Fetcher.refetch_object(o) + + assert %{ + "content" => "test 2", + "formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]} + } = refetched.data + end + + test "it adds to formerRepresentations if the remote does not have one and the object has changed", + %{object1: object1} do + full_object1 = + object1 + |> Map.merge(%{ + "content" => "mew mew #def", + "formerRepresentations" => %{ + "type" => "OrderedCollection", + "orderedItems" => [ + %{"type" => "Note", "content" => "mew mew 1"} + ], + "totalItems" => 1 + } + }) + + {:ok, o} = Object.create(full_object1) + + assert {:ok, refetched} = Fetcher.refetch_object(o) + + assert %{ + "content" => "test 1", + "formerRepresentations" => %{ + "orderedItems" => [ + %{"content" => "mew mew #def"}, + %{"content" => "mew mew 1"} + ], + "totalItems" => 2 + } + } = refetched.data + end + + test "it keeps the history intact if only updated time has changed", + %{object1: object1} do + full_object1 = + object1 + |> Map.merge(%{ + "updated" => "2023-05-08 23:43:47Z", + "formerRepresentations" => %{ + "type" => "OrderedCollection", + "orderedItems" => [ + %{"type" => "Note", "content" => "mew mew 1"} + ], + "totalItems" => 1 + } + }) + + {:ok, o} = Object.create(full_object1) + + assert {:ok, refetched} = Fetcher.refetch_object(o) + + assert %{ + "content" => "test 1", + "formerRepresentations" => %{ + "orderedItems" => [ + %{"content" => "mew mew 1"} + ], + "totalItems" => 1 + } + } = refetched.data + end + + test "it goes through ObjectValidator and MRF", %{object2: object2} do + with_mock Pleroma.Web.ActivityPub.MRF, [:passthrough], + filter: fn + %{"type" => "Note"} = object -> + {:ok, Map.put(object, "content", "MRFd content")} + + arg -> + passthrough([arg]) + end do + {:ok, o} = Object.create(object2) + + assert {:ok, refetched} = Fetcher.refetch_object(o) + + assert %{"content" => "MRFd content"} = refetched.data + end + end + end + + describe "fetch with history" do + setup do + object2 = %{ + "id" => "https://mastodon.social/2", + "actor" => "https://mastodon.social/users/emelie", + "attributedTo" => "https://mastodon.social/users/emelie", + "type" => "Note", + "content" => "test 2", + "bcc" => [], + "bto" => [], + "cc" => ["https://mastodon.social/users/emelie/followers"], + "to" => [], + "summary" => "", + "formerRepresentations" => %{ + "type" => "OrderedCollection", + "orderedItems" => [ + %{ + "type" => "Note", + "content" => "orig 2", + "actor" => "https://mastodon.social/users/emelie", + "attributedTo" => "https://mastodon.social/users/emelie", + "bcc" => [], + "bto" => [], + "cc" => ["https://mastodon.social/users/emelie/followers"], + "to" => [], + "summary" => "" + } + ], + "totalItems" => 1 + } + } + + mock(fn + %{ + method: :get, + url: "https://mastodon.social/2" + } -> + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/activity+json"}], + body: Jason.encode!(object2) + } + + %{ + method: :get, + url: "https://mastodon.social/users/emelie/collections/featured" + } -> + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/activity+json"}], + body: + Jason.encode!(%{ + "id" => "https://mastodon.social/users/emelie/collections/featured", + "type" => "OrderedCollection", + "actor" => "https://mastodon.social/users/emelie", + "attributedTo" => "https://mastodon.social/users/emelie", + "orderedItems" => [], + "totalItems" => 0 + }) + } + + env -> + apply(HttpRequestMock, :request, [env]) + end) + + %{object2: object2} + end + + test "it gets history", %{object2: object2} do + {:ok, object} = Fetcher.fetch_object_from_id(object2["id"]) + + assert %{ + "formerRepresentations" => %{ + "type" => "OrderedCollection", + "orderedItems" => [%{}] + } + } = object.data + end + end end diff --git a/test/pleroma/object/updater_test.exs b/test/pleroma/object/updater_test.exs new file mode 100644 index 000000000..7e9b44823 --- /dev/null +++ b/test/pleroma/object/updater_test.exs @@ -0,0 +1,76 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Object.UpdaterTest do + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + alias Pleroma.Object.Updater + + describe "make_update_object_data/3" do + setup do + note = insert(:note) + %{original_data: note.data} + end + + test "it makes an updated field", %{original_data: original_data} do + new_data = Map.put(original_data, "content", "new content") + + date = Pleroma.Web.ActivityPub.Utils.make_date() + update_object_data = Updater.make_update_object_data(original_data, new_data, date) + assert %{"updated" => ^date} = update_object_data + end + + test "it creates formerRepresentations", %{original_data: original_data} do + new_data = Map.put(original_data, "content", "new content") + + date = Pleroma.Web.ActivityPub.Utils.make_date() + update_object_data = Updater.make_update_object_data(original_data, new_data, date) + + history_item = original_data |> Map.drop(["id", "formerRepresentations"]) + + assert %{ + "formerRepresentations" => %{ + "totalItems" => 1, + "orderedItems" => [^history_item] + } + } = update_object_data + end + end + + describe "make_new_object_data_from_update_object/2" do + test "it reuses formerRepresentations if it exists" do + %{data: original_data} = insert(:note) + + new_data = + original_data + |> Map.put("content", "edited") + + date = Pleroma.Web.ActivityPub.Utils.make_date() + update_object_data = Updater.make_update_object_data(original_data, new_data, date) + + history = update_object_data["formerRepresentations"]["orderedItems"] + + update_object_data = + update_object_data + |> put_in( + ["formerRepresentations", "orderedItems"], + history ++ [Map.put(original_data, "summary", "additional summary")] + ) + |> put_in(["formerRepresentations", "totalItems"], length(history) + 1) + + %{ + updated_data: updated_data, + updated: updated, + used_history_in_new_object?: used_history_in_new_object? + } = Updater.make_new_object_data_from_update_object(original_data, update_object_data) + + assert updated + assert used_history_in_new_object? + assert updated_data["formerRepresentations"] == update_object_data["formerRepresentations"] + end + end +end diff --git a/test/pleroma/object_test.exs b/test/pleroma/object_test.exs index d536e0b16..2025d93e4 100644 --- a/test/pleroma/object_test.exs +++ b/test/pleroma/object_test.exs @@ -7,6 +7,7 @@ defmodule Pleroma.ObjectTest do use Oban.Testing, repo: Pleroma.Repo import ExUnit.CaptureLog + import Mox import Pleroma.Factory import Tesla.Mock @@ -15,10 +16,12 @@ defmodule Pleroma.ObjectTest do alias Pleroma.Object alias Pleroma.Repo alias Pleroma.Tests.ObanHelpers + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.Web.CommonAPI setup do mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + ConfigMock |> stub_with(Pleroma.Test.StaticConfig) :ok end @@ -444,4 +447,42 @@ defmodule Pleroma.ObjectTest do Enum.sort_by(object.hashtags, & &1.name) end end + + describe "get_emoji_reactions/1" do + test "3-tuple current format" do + object = %Object{ + data: %{ + "reactions" => [ + ["x", ["https://some/user"], "https://some/emoji"] + ] + } + } + + assert Object.get_emoji_reactions(object) == object.data["reactions"] + end + + test "2-tuple legacy format" do + object = %Object{ + data: %{ + "reactions" => [ + ["x", ["https://some/user"]] + ] + } + } + + assert Object.get_emoji_reactions(object) == [["x", ["https://some/user"], nil]] + end + + test "Map format" do + object = %Object{ + data: %{ + "reactions" => %{ + "x" => ["https://some/user"] + } + } + } + + assert Object.get_emoji_reactions(object) == [["x", ["https://some/user"], nil]] + end + end end diff --git a/test/pleroma/otp_version_test.exs b/test/pleroma/otp_version_test.exs index 642cd1310..21701d5a8 100644 --- a/test/pleroma/otp_version_test.exs +++ b/test/pleroma/otp_version_test.exs @@ -28,7 +28,7 @@ defmodule Pleroma.OTPVersionTest do "23.0" end - test "with non existance file" do + 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" diff --git a/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs b/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs index 52a606368..9847781f0 100644 --- a/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs +++ b/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs @@ -29,13 +29,13 @@ defmodule Pleroma.Repo.Migrations.AutolinkerToLinkifyTest do %{value: new_opts} = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Formatter}) - assert new_opts == [ + assert Keyword.equal?(new_opts, class: false, extra: true, new_window: false, rel: "testing", strip_prefix: false - ] + ) clear_config(Pleroma.Formatter, new_opts) assert new_opts == Pleroma.Config.get(Pleroma.Formatter) @@ -67,6 +67,6 @@ defmodule Pleroma.Repo.Migrations.AutolinkerToLinkifyTest do strip_prefix: false ] - assert migration.transform_opts(old_opts) == expected_opts + assert Keyword.equal?(migration.transform_opts(old_opts), expected_opts) end end diff --git a/test/pleroma/repo/migrations/fix_malformed_formatter_config_test.exs b/test/pleroma/repo/migrations/fix_malformed_formatter_config_test.exs index 4c45adb4b..cf3fe5aac 100644 --- a/test/pleroma/repo/migrations/fix_malformed_formatter_config_test.exs +++ b/test/pleroma/repo/migrations/fix_malformed_formatter_config_test.exs @@ -26,13 +26,13 @@ defmodule Pleroma.Repo.Migrations.FixMalformedFormatterConfigTest do %{value: new_opts} = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Formatter}) - assert new_opts == [ + assert Keyword.equal?(new_opts, class: false, extra: true, new_window: false, rel: "F", strip_prefix: false - ] + ) clear_config(Pleroma.Formatter, new_opts) assert new_opts == Pleroma.Config.get(Pleroma.Formatter) diff --git a/test/pleroma/resilience_test.exs b/test/pleroma/resilience_test.exs new file mode 100644 index 000000000..9dc5d0dd6 --- /dev/null +++ b/test/pleroma/resilience_test.exs @@ -0,0 +1,103 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ResilienceTest do + use Pleroma.Web.ConnCase, async: true + + import Pleroma.Factory + + alias Pleroma.Activity + alias Pleroma.Repo + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MastodonAPI.StatusView + + setup do + # user = insert(:user) + %{user: user, conn: conn} = oauth_access(["write", "read"]) + other_user = insert(:user) + + {:ok, post_one} = CommonAPI.post(user, %{status: "Here is a post"}) + {:ok, like} = CommonAPI.favorite(other_user, post_one.id) + + %{ + user: user, + other_user: other_user, + post_one: post_one, + like: like, + conn: conn + } + end + + test "after destruction of like activities, things still work", %{ + user: user, + post_one: post, + other_user: other_user, + conn: conn, + like: like + } do + post = Repo.get(Activity, post.id) + + # Rendering the liked status + rendered_for_user = StatusView.render("show.json", %{activity: post, for: user}) + assert rendered_for_user.favourites_count == 1 + + rendered_for_other_user = StatusView.render("show.json", %{activity: post, for: other_user}) + assert rendered_for_other_user.favourites_count == 1 + assert rendered_for_other_user.favourited + + # Getting the favourited by + [liking_user] = + conn + |> get("/api/v1/statuses/#{post.id}/favourited_by") + |> json_response(200) + + assert liking_user["id"] == other_user.id + + # We have one notification + [notification] = + conn + |> get("/api/v1/notifications") + |> json_response(200) + + assert notification["type"] == "favourite" + + # Destroying the like + Repo.delete(like) + post = Repo.get(Activity, post.id) + + # Rendering the liked status + rendered_for_user = StatusView.render("show.json", %{activity: post, for: user}) + assert rendered_for_user.favourites_count == 1 + + rendered_for_other_user = StatusView.render("show.json", %{activity: post, for: other_user}) + assert rendered_for_other_user.favourites_count == 1 + assert rendered_for_other_user.favourited + + # Getting the favourited by + [liking_user] = + conn + |> get("/api/v1/statuses/#{post.id}/favourited_by") + |> json_response(200) + + assert liking_user["id"] == other_user.id + + # Notification is removed + + assert [] == + conn + |> get("/api/v1/notifications") + |> json_response(200) + + # Favoriting again doesn't hurt + {:ok, _like_two} = CommonAPI.favorite(other_user, post.id) + + post = Repo.get(Activity, post.id) + + # Rendering the liked status + rendered_for_user = StatusView.render("show.json", %{activity: post, for: user}) + assert rendered_for_user.favourites_count == 1 + + # General fallout: Can't unfavorite stuff anymore. Acceptable for remote users. + end +end diff --git a/test/pleroma/reverse_proxy_test.exs b/test/pleroma/reverse_proxy_test.exs index 0bd4db8d1..fb330232a 100644 --- a/test/pleroma/reverse_proxy_test.exs +++ b/test/pleroma/reverse_proxy_test.exs @@ -306,7 +306,7 @@ defmodule Pleroma.ReverseProxyTest do end describe "response content disposition header" do - test "not atachment", %{conn: conn} do + test "not attachment", %{conn: conn} do disposition_headers_mock([ {"content-type", "image/gif"}, {"content-length", "0"} diff --git a/test/pleroma/rule_test.exs b/test/pleroma/rule_test.exs new file mode 100644 index 000000000..d710a6312 --- /dev/null +++ b/test/pleroma/rule_test.exs @@ -0,0 +1,57 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.RuleTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Repo + alias Pleroma.Rule + + test "getting a list of rules sorted by priority" do + %{id: id1} = Rule.create(%{text: "Example rule"}) + %{id: id2} = Rule.create(%{text: "Second rule", priority: 2}) + %{id: id3} = Rule.create(%{text: "Third rule", priority: 1}) + + rules = + Rule.query() + |> Repo.all() + + assert [%{id: ^id1}, %{id: ^id3}, %{id: ^id2}] = rules + end + + test "creating rules" do + %{id: id} = Rule.create(%{text: "Example rule"}) + + assert %{text: "Example rule"} = Rule.get(id) + end + + test "editing rules" do + %{id: id} = Rule.create(%{text: "Example rule"}) + + Rule.update(%{text: "There are no rules", priority: 2}, id) + + assert %{text: "There are no rules", priority: 2} = Rule.get(id) + end + + test "deleting rules" do + %{id: id} = Rule.create(%{text: "Example rule"}) + + Rule.delete(id) + + assert [] = + Rule.query() + |> Pleroma.Repo.all() + end + + test "getting rules by ids" do + %{id: id1} = Rule.create(%{text: "Example rule"}) + %{id: id2} = Rule.create(%{text: "Second rule"}) + %{id: _id3} = Rule.create(%{text: "Third rule"}) + + rules = Rule.get([id1, id2]) + + assert Enum.all?(rules, &(&1.id in [id1, id2])) + assert length(rules) == 2 + end +end diff --git a/test/pleroma/scheduled_activity_test.exs b/test/pleroma/scheduled_activity_test.exs index 3a0927d3f..4818e8bcf 100644 --- a/test/pleroma/scheduled_activity_test.exs +++ b/test/pleroma/scheduled_activity_test.exs @@ -3,19 +3,23 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.ScheduledActivityTest do - use Pleroma.DataCase + use Pleroma.DataCase, async: true alias Pleroma.ScheduledActivity + alias Pleroma.Test.StaticConfig + alias Pleroma.UnstubbedConfigMock, as: ConfigMock + import Mox import Pleroma.Factory - setup do: clear_config([ScheduledActivity, :enabled]) - - setup [:ensure_local_uploader] - describe "creation" do test "scheduled activities with jobs when ScheduledActivity enabled" do - clear_config([ScheduledActivity, :enabled], true) + ConfigMock + |> stub(:get, fn + [ScheduledActivity, :enabled] -> true + path -> StaticConfig.get(path) + end) + user = insert(:user) today = @@ -34,7 +38,12 @@ defmodule Pleroma.ScheduledActivityTest do end test "scheduled activities without jobs when ScheduledActivity disabled" do - clear_config([ScheduledActivity, :enabled], false) + ConfigMock + |> stub(:get, fn + [ScheduledActivity, :enabled] -> false + path -> StaticConfig.get(path) + end) + user = insert(:user) today = @@ -53,6 +62,9 @@ defmodule Pleroma.ScheduledActivityTest do end test "when daily user limit is exceeded" do + ConfigMock + |> stub_with(StaticConfig) + user = insert(:user) today = @@ -69,6 +81,9 @@ defmodule Pleroma.ScheduledActivityTest do end test "when total user limit is exceeded" do + ConfigMock + |> stub_with(StaticConfig) + user = insert(:user) today = @@ -89,6 +104,9 @@ defmodule Pleroma.ScheduledActivityTest do end test "when scheduled_at is earlier than 5 minute from now" do + ConfigMock + |> stub_with(StaticConfig) + user = insert(:user) scheduled_at = diff --git a/test/pleroma/activity/search_test.exs b/test/pleroma/search/database_search_test.exs index b8096fe73..d8dd09063 100644 --- a/test/pleroma/activity/search_test.exs +++ b/test/pleroma/search/database_search_test.exs @@ -2,8 +2,8 @@ # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -defmodule Pleroma.Activity.SearchTest do - alias Pleroma.Activity.Search +defmodule Pleroma.Search.DatabaseSearchTest do + alias Pleroma.Search.DatabaseSearch, as: Search alias Pleroma.Web.CommonAPI import Pleroma.Factory @@ -18,21 +18,23 @@ defmodule Pleroma.Activity.SearchTest do assert result.id == post.id end - test "using plainto_tsquery on postgres < 11" do - old_version = :persistent_term.get({Pleroma.Repo, :postgres_version}) - :persistent_term.put({Pleroma.Repo, :postgres_version}, 10.0) - on_exit(fn -> :persistent_term.put({Pleroma.Repo, :postgres_version}, old_version) end) - + test "it finds local-only posts for authenticated users" do user = insert(:user) - {:ok, post} = CommonAPI.post(user, %{status: "it's wednesday my dudes"}) - {:ok, _post2} = CommonAPI.post(user, %{status: "it's wednesday my bros"}) + reader = insert(:user) + {:ok, post} = CommonAPI.post(user, %{status: "it's wednesday my dudes", visibility: "local"}) - # plainto doesn't understand complex queries - assert [result] = Search.search(nil, "wednesday -dudes") + [result] = Search.search(reader, "wednesday") assert result.id == post.id end + test "it does not find local-only posts for anonymous users" do + user = insert(:user) + {:ok, _post} = CommonAPI.post(user, %{status: "it's wednesday my dudes", visibility: "local"}) + + assert [] = Search.search(nil, "wednesday") + end + test "using websearch_to_tsquery" do user = insert(:user) {:ok, _post} = CommonAPI.post(user, %{status: "it's wednesday my dudes"}) diff --git a/test/pleroma/search/healthcheck_test.exs b/test/pleroma/search/healthcheck_test.exs new file mode 100644 index 000000000..e7649d949 --- /dev/null +++ b/test/pleroma/search/healthcheck_test.exs @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Search.HealthcheckTest do + use Pleroma.DataCase + + import Tesla.Mock + + alias Pleroma.Search.Healthcheck + + @good1 "http://good1.example.com/healthz" + @good2 "http://good2.example.com/health" + @bad "http://bad.example.com/healthy" + + setup do + mock(fn + %{method: :get, url: @good1} -> + %Tesla.Env{ + status: 200, + body: "" + } + + %{method: :get, url: @good2} -> + %Tesla.Env{ + status: 200, + body: "" + } + + %{method: :get, url: @bad} -> + %Tesla.Env{ + status: 503, + body: "" + } + end) + + :ok + end + + test "true for 200 responses" do + assert Healthcheck.check([@good1]) + assert Healthcheck.check([@good1, @good2]) + end + + test "false if any response is not a 200" do + refute Healthcheck.check([@bad]) + refute Healthcheck.check([@good1, @bad]) + end +end diff --git a/test/pleroma/search/meilisearch_test.exs b/test/pleroma/search/meilisearch_test.exs new file mode 100644 index 000000000..eea454323 --- /dev/null +++ b/test/pleroma/search/meilisearch_test.exs @@ -0,0 +1,160 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Search.MeilisearchTest do + require Pleroma.Constants + + use Pleroma.DataCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + import Tesla.Mock + import Mox + + alias Pleroma.Search.Meilisearch + alias Pleroma.UnstubbedConfigMock, as: Config + alias Pleroma.Web.CommonAPI + alias Pleroma.Workers.SearchIndexingWorker + + describe "meilisearch" do + test "indexes a local post on creation" do + user = insert(:user) + + Tesla.Mock.mock(fn + %{ + method: :put, + url: "http://127.0.0.1:7700/indexes/objects/documents", + body: body + } -> + assert match?( + [%{"content" => "guys i just don't wanna leave the swamp"}], + Jason.decode!(body) + ) + + # To make sure that the worker is called + send(self(), "posted_to_meilisearch") + + %{ + "enqueuedAt" => "2023-11-12T12:36:46.927517Z", + "indexUid" => "objects", + "status" => "enqueued", + "taskUid" => 6, + "type" => "documentAdditionOrUpdate" + } + |> json() + end) + + Config + |> expect(:get, 3, fn + [Pleroma.Search, :module], nil -> + Meilisearch + + [Pleroma.Search.Meilisearch, :url], nil -> + "http://127.0.0.1:7700" + + [Pleroma.Search.Meilisearch, :private_key], nil -> + "secret" + end) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "guys i just don't wanna leave the swamp", + visibility: "public" + }) + + args = %{"op" => "add_to_index", "activity" => activity.id} + + assert_enqueued( + worker: SearchIndexingWorker, + args: args + ) + + assert :ok = perform_job(SearchIndexingWorker, args) + assert_received("posted_to_meilisearch") + end + + test "doesn't index posts that are not public" do + user = insert(:user) + + Enum.each(["private", "direct"], fn visibility -> + {:ok, activity} = + CommonAPI.post(user, %{ + status: "guys i just don't wanna leave the swamp", + visibility: visibility + }) + + args = %{"op" => "add_to_index", "activity" => activity.id} + + Config + |> expect(:get, fn + [Pleroma.Search, :module], nil -> + Meilisearch + end) + + assert_enqueued(worker: SearchIndexingWorker, args: args) + assert :ok = perform_job(SearchIndexingWorker, args) + end) + end + + test "deletes posts from index when deleted locally" do + user = insert(:user) + + Tesla.Mock.mock(fn + %{ + method: :put, + url: "http://127.0.0.1:7700/indexes/objects/documents", + body: body + } -> + assert match?( + [%{"content" => "guys i just don't wanna leave the swamp"}], + Jason.decode!(body) + ) + + %{ + "enqueuedAt" => "2023-11-12T12:36:46.927517Z", + "indexUid" => "objects", + "status" => "enqueued", + "taskUid" => 6, + "type" => "documentAdditionOrUpdate" + } + |> json() + + %{method: :delete, url: "http://127.0.0.1:7700/indexes/objects/documents/" <> id} -> + send(self(), "called_delete") + assert String.length(id) > 1 + json(%{}) + end) + + Config + |> expect(:get, 6, fn + [Pleroma.Search, :module], nil -> + Meilisearch + + [Pleroma.Search.Meilisearch, :url], nil -> + "http://127.0.0.1:7700" + + [Pleroma.Search.Meilisearch, :private_key], nil -> + "secret" + end) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "guys i just don't wanna leave the swamp", + visibility: "public" + }) + + args = %{"op" => "add_to_index", "activity" => activity.id} + assert_enqueued(worker: SearchIndexingWorker, args: args) + assert :ok = perform_job(SearchIndexingWorker, args) + + {:ok, _} = CommonAPI.delete(activity.id, user) + + delete_args = %{"op" => "remove_from_index", "object" => activity.object.id} + assert_enqueued(worker: SearchIndexingWorker, args: delete_args) + assert :ok = perform_job(SearchIndexingWorker, delete_args) + + assert_received("called_delete") + end + end +end diff --git a/test/pleroma/signature_test.exs b/test/pleroma/signature_test.exs index 92d05f26c..8edf67a7b 100644 --- a/test/pleroma/signature_test.exs +++ b/test/pleroma/signature_test.exs @@ -43,10 +43,7 @@ defmodule Pleroma.SignatureTest do end test "it returns error when not found user" do - assert capture_log(fn -> - assert Signature.fetch_public_key(make_fake_conn("https://test-ap-id")) == - {:error, :error} - end) =~ "[error] Could not decode user" + assert Signature.fetch_public_key(make_fake_conn("https://test-ap-id")) == {:error, :error} end test "it returns error if public key is nil" do @@ -109,9 +106,14 @@ defmodule Pleroma.SignatureTest do {:ok, "https://example.com/users/1234"} end + test "it deduces the actor id for gotoSocial" do + assert Signature.key_id_to_actor_id("https://example.com/users/1234/main-key") == + {:ok, "https://example.com/users/1234"} + end + test "it calls webfinger for 'acct:' accounts" do with_mock(Pleroma.Web.WebFinger, - finger: fn _ -> %{"ap_id" => "https://gensokyo.2hu/users/raymoo"} end + finger: fn _ -> {:ok, %{"ap_id" => "https://gensokyo.2hu/users/raymoo"}} end ) do assert Signature.key_id_to_actor_id("acct:raymoo@gensokyo.2hu") == {:ok, "https://gensokyo.2hu/users/raymoo"} diff --git a/test/pleroma/upload/filter/analyze_metadata_test.exs b/test/pleroma/upload/filter/analyze_metadata_test.exs index b800a4a43..e4ac673b2 100644 --- a/test/pleroma/upload/filter/analyze_metadata_test.exs +++ b/test/pleroma/upload/filter/analyze_metadata_test.exs @@ -20,6 +20,20 @@ defmodule Pleroma.Upload.Filter.AnalyzeMetadataTest do assert meta.blurhash end + test "it blurhashes images with an alpha component" do + upload = %Pleroma.Upload{ + name: "an… image.jpg", + content_type: "image/jpeg", + path: Path.absname("test/fixtures/png_with_transparency.png"), + tempfile: Path.absname("test/fixtures/png_with_transparency.png") + } + + {:ok, :filtered, meta} = AnalyzeMetadata.filter(upload) + + assert %{width: 320, height: 320} = meta + assert meta.blurhash == "eXJi-E:SwCEm5rCmn$+YWYn+15K#5A$xxCi{SiV]s*W:Efa#s.jE-T" + end + test "adds the dimensions for videos" do upload = %Pleroma.Upload{ name: "coolvideo.mp4", diff --git a/test/pleroma/upload/filter/exiftool/read_description_test.exs b/test/pleroma/upload/filter/exiftool/read_description_test.exs index 7389fda47..9a1bd61d7 100644 --- a/test/pleroma/upload/filter/exiftool/read_description_test.exs +++ b/test/pleroma/upload/filter/exiftool/read_description_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Upload.Filter.Exiftool.ReadDescriptionTest do - use Pleroma.DataCase, async: true + use Pleroma.DataCase alias Pleroma.Upload.Filter @uploads %Pleroma.Upload{ @@ -42,6 +42,33 @@ defmodule Pleroma.Upload.Filter.Exiftool.ReadDescriptionTest do {:ok, :filtered, uploads_after} end + test "Ignores warnings" do + uploads = %Pleroma.Upload{ + name: "image_with_imagedescription_and_caption-abstract_and_stray_data_after.png", + content_type: "image/png", + path: + Path.absname( + "test/fixtures/image_with_imagedescription_and_caption-abstract_and_stray_data_after.png" + ), + tempfile: + Path.absname( + "test/fixtures/image_with_imagedescription_and_caption-abstract_and_stray_data_after.png" + ) + } + + assert {:ok, :filtered, %{description: "a descriptive white pixel"}} = + Filter.Exiftool.ReadDescription.filter(uploads) + + uploads = %Pleroma.Upload{ + name: "image_with_stray_data_after.png", + content_type: "image/png", + path: Path.absname("test/fixtures/image_with_stray_data_after.png"), + tempfile: Path.absname("test/fixtures/image_with_stray_data_after.png") + } + + assert {:ok, :filtered, %{description: nil}} = Filter.Exiftool.ReadDescription.filter(uploads) + end + test "otherwise returns iptc:Caption-Abstract when present" do upload = %Pleroma.Upload{ name: "image_with_caption-abstract.jpg", diff --git a/test/pleroma/upload/filter/exiftool/strip_location_test.exs b/test/pleroma/upload/filter/exiftool/strip_location_test.exs index 7e1541f60..bcb5f3f60 100644 --- a/test/pleroma/upload/filter/exiftool/strip_location_test.exs +++ b/test/pleroma/upload/filter/exiftool/strip_location_test.exs @@ -31,12 +31,19 @@ defmodule Pleroma.Upload.Filter.Exiftool.StripLocationTest do refute String.match?(exif_filtered, ~r/GPS/) end - test "verify webp files are skipped" do - upload = %Pleroma.Upload{ - name: "sample.webp", - content_type: "image/webp" - } - - assert Filter.Exiftool.StripLocation.filter(upload) == {:ok, :noop} + test "verify webp, heic, svg files are skipped" do + uploads = + ~w{webp heic svg svg+xml} + |> Enum.map(fn type -> + %Pleroma.Upload{ + name: "sample.#{type}", + content_type: "image/#{type}" + } + end) + + uploads + |> Enum.each(fn upload -> + assert Filter.Exiftool.StripLocation.filter(upload) == {:ok, :noop} + end) end end diff --git a/test/pleroma/upload/filter/only_media_test.exs b/test/pleroma/upload/filter/only_media_test.exs new file mode 100644 index 000000000..75be070a1 --- /dev/null +++ b/test/pleroma/upload/filter/only_media_test.exs @@ -0,0 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Upload.Filter.OnlyMediaTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Upload + alias Pleroma.Upload.Filter.OnlyMedia + + test "Allows media Content-Type" do + ["audio/mpeg", "image/jpeg", "video/mp4"] + |> Enum.each(fn type -> + upload = %Upload{ + content_type: type + } + + assert {:ok, :noop} = OnlyMedia.filter(upload) + end) + end + + test "Disallows non-media Content-Type" do + ["application/javascript", "application/pdf", "text/html"] + |> Enum.each(fn type -> + upload = %Upload{ + content_type: type + } + + assert {:error, _} = OnlyMedia.filter(upload) + end) + end +end diff --git a/test/pleroma/upload_test.exs b/test/pleroma/upload_test.exs index f2795f985..facb634c3 100644 --- a/test/pleroma/upload_test.exs +++ b/test/pleroma/upload_test.exs @@ -6,10 +6,19 @@ defmodule Pleroma.UploadTest do use Pleroma.DataCase import ExUnit.CaptureLog + import Mox + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.Upload alias Pleroma.Uploaders.Uploader + setup do + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + + :ok + end + @upload_file %Plug.Upload{ content_type: "image/jpeg", path: Path.absname("test/fixtures/image_tmp.jpg"), @@ -49,20 +58,22 @@ defmodule Pleroma.UploadTest do test "it returns file" do File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - assert Upload.store(@upload_file) == - {:ok, - %{ - "name" => "image.jpg", - "type" => "Document", - "mediaType" => "image/jpeg", - "url" => [ - %{ - "href" => "http://localhost:4001/media/post-process-file.jpg", - "mediaType" => "image/jpeg", - "type" => "Link" - } - ] - }} + assert {:ok, result} = Upload.store(@upload_file) + + assert result == + %{ + "id" => result["id"], + "name" => "image.jpg", + "type" => "Document", + "mediaType" => "image/jpeg", + "url" => [ + %{ + "href" => "http://localhost:4001/media/post-process-file.jpg", + "mediaType" => "image/jpeg", + "type" => "Link" + } + ] + } Task.await(Agent.get(TestUploaderSuccess, fn task_pid -> task_pid end)) end @@ -234,6 +245,8 @@ defmodule Pleroma.UploadTest do describe "Setting a custom base_url for uploaded media" do setup do: clear_config([Pleroma.Upload, :base_url], "https://cache.pleroma.social") + # This seems to be backwards. Skipped for that reason + @tag skip: true test "returns a media url with configured base_url" do base_url = Pleroma.Config.get([Pleroma.Upload, :base_url]) diff --git a/test/pleroma/uploaders/s3_test.exs b/test/pleroma/uploaders/s3_test.exs index d870449b1..b8df0e65a 100644 --- a/test/pleroma/uploaders/s3_test.exs +++ b/test/pleroma/uploaders/s3_test.exs @@ -3,22 +3,27 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Uploaders.S3Test do - use Pleroma.DataCase + use Pleroma.DataCase, async: true + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.Uploaders.S3 + alias Pleroma.Uploaders.S3.ExAwsMock - import Mock + import Mox import ExUnit.CaptureLog - setup do - clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.S3) - clear_config([Pleroma.Upload, :base_url], "https://s3.amazonaws.com") - clear_config([Pleroma.Uploaders.S3]) - clear_config([Pleroma.Uploaders.S3, :bucket], "test_bucket") - end - describe "get_file/1" do - test "it returns path to local folder for files" do + test "it returns url for files" do + ConfigMock + |> expect(:get, 6, fn key -> + [ + {Pleroma.Upload, + [uploader: Pleroma.Uploaders.S3, base_url: "https://s3.amazonaws.com"]}, + {Pleroma.Uploaders.S3, [bucket: "test_bucket"]} + ] + |> get_in(key) + end) + assert S3.get_file("test_image.jpg") == { :ok, {:url, "https://s3.amazonaws.com/test_bucket/test_image.jpg"} @@ -26,13 +31,16 @@ defmodule Pleroma.Uploaders.S3Test do end test "it returns path without bucket when truncated_namespace set to ''" do - clear_config([Pleroma.Uploaders.S3], - bucket: "test_bucket", - bucket_namespace: "myaccount", - truncated_namespace: "" - ) - - clear_config([Pleroma.Upload, :base_url], "https://s3.amazonaws.com") + ConfigMock + |> expect(:get, 6, fn key -> + [ + {Pleroma.Upload, + [uploader: Pleroma.Uploaders.S3, base_url: "https://s3.amazonaws.com"]}, + {Pleroma.Uploaders.S3, + [bucket: "test_bucket", truncated_namespace: "", bucket_namespace: "myaccount"]} + ] + |> get_in(key) + end) assert S3.get_file("test_image.jpg") == { :ok, @@ -41,10 +49,15 @@ defmodule Pleroma.Uploaders.S3Test do end test "it returns path with bucket namespace when namespace is set" do - clear_config([Pleroma.Uploaders.S3], - bucket: "test_bucket", - bucket_namespace: "family" - ) + ConfigMock + |> expect(:get, 6, fn key -> + [ + {Pleroma.Upload, + [uploader: Pleroma.Uploaders.S3, base_url: "https://s3.amazonaws.com"]}, + {Pleroma.Uploaders.S3, [bucket: "test_bucket", bucket_namespace: "family"]} + ] + |> get_in(key) + end) assert S3.get_file("test_image.jpg") == { :ok, @@ -62,28 +75,42 @@ defmodule Pleroma.Uploaders.S3Test do tempfile: Path.absname("test/instance_static/add/shortcode.png") } + ConfigMock + |> expect(:get, fn [Pleroma.Uploaders.S3] -> + [ + bucket: "test_bucket" + ] + end) + [file_upload: file_upload] end test "save file", %{file_upload: file_upload} do - with_mock ExAws, request: fn _ -> {:ok, :ok} end do - assert S3.put_file(file_upload) == {:ok, {:file, "test_folder/image-tet.jpg"}} - end + ExAwsMock + |> expect(:request, fn _req -> {:ok, %{status_code: 200}} end) + + assert S3.put_file(file_upload) == {:ok, {:file, "test_folder/image-tet.jpg"}} end test "returns error", %{file_upload: file_upload} do - with_mock ExAws, request: fn _ -> {:error, "S3 Upload failed"} end do - assert capture_log(fn -> - assert S3.put_file(file_upload) == {:error, "S3 Upload failed"} - end) =~ "Elixir.Pleroma.Uploaders.S3: {:error, \"S3 Upload failed\"}" - end + ExAwsMock + |> expect(:request, fn _req -> {:error, "S3 Upload failed"} end) + + assert capture_log(fn -> + assert S3.put_file(file_upload) == {:error, "S3 Upload failed"} + end) =~ "Elixir.Pleroma.Uploaders.S3: {:error, \"S3 Upload failed\"}" end end describe "delete_file/1" do - test_with_mock "deletes file", ExAws, request: fn _req -> {:ok, %{status_code: 204}} end do + test "deletes file" do + ExAwsMock + |> expect(:request, fn _req -> {:ok, %{status_code: 204}} end) + + ConfigMock + |> expect(:get, fn [Pleroma.Uploaders.S3, :bucket] -> "test_bucket" end) + assert :ok = S3.delete_file("image.jpg") - assert_called(ExAws.request(:_)) end end end diff --git a/test/pleroma/user/backup_async_test.exs b/test/pleroma/user/backup_async_test.exs new file mode 100644 index 000000000..76716d684 --- /dev/null +++ b/test/pleroma/user/backup_async_test.exs @@ -0,0 +1,51 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.BackupAsyncTest do + use Pleroma.DataCase, async: true + + import Pleroma.Factory + import Mox + + alias Pleroma.UnstubbedConfigMock, as: ConfigMock + alias Pleroma.User.Backup + alias Pleroma.User.Backup.ProcessorMock + + setup do + user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"}) + + {:ok, backup} = user |> Backup.new() |> Repo.insert() + %{backup: backup} + end + + @tag capture_log: true + test "it handles unrecoverable exceptions", %{backup: backup} do + ProcessorMock + |> expect(:do_process, fn _, _ -> + raise "mock exception" + end) + + ConfigMock + |> stub_with(Pleroma.Config) + + {:error, %{backup: backup, reason: :exit}} = Backup.process(backup, ProcessorMock) + + assert backup.state == :failed + end + + @tag capture_log: true + test "it handles timeouts", %{backup: backup} do + ProcessorMock + |> expect(:do_process, fn _, _ -> + Process.sleep(:timer.seconds(4)) + end) + + ConfigMock + |> expect(:get, fn [Pleroma.User.Backup, :process_wait_time] -> :timer.seconds(2) end) + + {:error, %{backup: backup, reason: :timeout}} = Backup.process(backup, ProcessorMock) + + assert backup.state == :failed + end +end diff --git a/test/pleroma/user/backup_test.exs b/test/pleroma/user/backup_test.exs index 6441c5ba8..5503d15bc 100644 --- a/test/pleroma/user/backup_test.exs +++ b/test/pleroma/user/backup_test.exs @@ -9,10 +9,14 @@ defmodule Pleroma.User.BackupTest do import Mock import Pleroma.Factory import Swoosh.TestAssertions + import Mox alias Pleroma.Bookmark alias Pleroma.Tests.ObanHelpers + alias Pleroma.UnstubbedConfigMock, as: ConfigMock + alias Pleroma.Uploaders.S3.ExAwsMock alias Pleroma.User.Backup + alias Pleroma.User.Backup.ProcessorMock alias Pleroma.Web.CommonAPI alias Pleroma.Workers.BackupWorker @@ -20,17 +24,25 @@ defmodule Pleroma.User.BackupTest do clear_config([Pleroma.Upload, :uploader]) clear_config([Backup, :limit_days]) clear_config([Pleroma.Emails.Mailer, :enabled], true) + + ConfigMock + |> stub_with(Pleroma.Config) + + ProcessorMock + |> stub_with(Pleroma.User.Backup.Processor) + + :ok end - test "it requries enabled email" do + test "it does not requrie enabled email" do clear_config([Pleroma.Emails.Mailer, :enabled], false) user = insert(:user) - assert {:error, "Backups require enabled email"} == Backup.create(user) + assert {:ok, _} = Backup.create(user) end - test "it requries user's email" do + test "it does not require user's email" do user = insert(:user, %{email: nil}) - assert {:error, "Email is required"} == Backup.create(user) + assert {:ok, _} = Backup.create(user) end test "it creates a backup record and an Oban job" do @@ -39,7 +51,7 @@ defmodule Pleroma.User.BackupTest do assert_enqueued(worker: BackupWorker, args: args) backup = Backup.get(args["backup_id"]) - assert %Backup{user_id: ^user_id, processed: false, file_size: 0} = backup + assert %Backup{user_id: ^user_id, processed: false, file_size: 0, state: :pending} = backup end test "it return an error if the export limit is over" do @@ -59,7 +71,7 @@ defmodule Pleroma.User.BackupTest do assert {:ok, %Oban.Job{args: %{"backup_id" => backup_id} = args}} = Backup.create(user) assert {:ok, backup} = perform_job(BackupWorker, args) assert backup.file_size > 0 - assert %Backup{id: ^backup_id, processed: true, user_id: ^user_id} = backup + assert %Backup{id: ^backup_id, processed: true, user_id: ^user_id, state: :complete} = backup delete_job_args = %{"op" => "delete", "backup_id" => backup_id} @@ -75,6 +87,66 @@ defmodule Pleroma.User.BackupTest do ) end + test "it updates states of the backup" do + clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + %{id: user_id} = user = insert(:user) + + assert {:ok, %Oban.Job{args: %{"backup_id" => backup_id} = args}} = Backup.create(user) + assert {:ok, backup} = perform_job(BackupWorker, args) + assert backup.file_size > 0 + assert %Backup{id: ^backup_id, processed: true, user_id: ^user_id, state: :complete} = backup + + delete_job_args = %{"op" => "delete", "backup_id" => backup_id} + + assert_enqueued(worker: BackupWorker, args: delete_job_args) + assert {:ok, backup} = perform_job(BackupWorker, delete_job_args) + refute Backup.get(backup_id) + + email = Pleroma.Emails.UserEmail.backup_is_ready_email(backup) + + assert_email_sent( + to: {user.name, user.email}, + html_body: email.html_body + ) + end + + test "it does not send an email if the user does not have an email" do + clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + %{id: user_id} = user = insert(:user, %{email: nil}) + + assert {:ok, %Oban.Job{args: %{"backup_id" => backup_id} = args}} = Backup.create(user) + assert {:ok, backup} = perform_job(BackupWorker, args) + assert backup.file_size > 0 + assert %Backup{id: ^backup_id, processed: true, user_id: ^user_id} = backup + + assert_no_email_sent() + end + + test "it does not send an email if mailer is not on" do + clear_config([Pleroma.Emails.Mailer, :enabled], false) + clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + %{id: user_id} = user = insert(:user) + + assert {:ok, %Oban.Job{args: %{"backup_id" => backup_id} = args}} = Backup.create(user) + assert {:ok, backup} = perform_job(BackupWorker, args) + assert backup.file_size > 0 + assert %Backup{id: ^backup_id, processed: true, user_id: ^user_id} = backup + + assert_no_email_sent() + end + + test "it does not send an email if the user has an empty email" do + clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + %{id: user_id} = user = insert(:user, %{email: ""}) + + assert {:ok, %Oban.Job{args: %{"backup_id" => backup_id} = args}} = Backup.create(user) + assert {:ok, backup} = perform_job(BackupWorker, args) + assert backup.file_size > 0 + assert %Backup{id: ^backup_id, processed: true, user_id: ^user_id} = backup + + assert_no_email_sent() + end + test "it removes outdated backups after creating a fresh one" do clear_config([Backup, :limit_days], -1) clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) @@ -94,6 +166,7 @@ defmodule Pleroma.User.BackupTest do test "it creates a zip archive with user data" do user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"}) + %{ap_id: other_ap_id} = other_user = insert(:user) {:ok, %{object: %{data: %{"id" => id1}}} = status1} = CommonAPI.post(user, %{status: "status1"}) @@ -110,8 +183,10 @@ defmodule Pleroma.User.BackupTest do Bookmark.create(user.id, status2.id) Bookmark.create(user.id, status3.id) + CommonAPI.follow(user, other_user) + assert {:ok, backup} = user |> Backup.new() |> Repo.insert() - assert {:ok, path} = Backup.export(backup) + assert {:ok, path} = Backup.export(backup, self()) assert {:ok, zipfile} = :zip.zip_open(String.to_charlist(path), [:memory]) assert {:ok, {'actor.json', json}} = :zip.zip_get('actor.json', zipfile) @@ -189,10 +264,69 @@ defmodule Pleroma.User.BackupTest do "type" => "OrderedCollection" } = Jason.decode!(json) + assert {:ok, {'following.json', json}} = :zip.zip_get('following.json', zipfile) + + assert %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "following.json", + "orderedItems" => [^other_ap_id], + "totalItems" => 1, + "type" => "OrderedCollection" + } = Jason.decode!(json) + :zip.zip_close(zipfile) File.rm!(path) end + test "it counts the correct number processed" do + user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"}) + + Enum.map(1..120, fn i -> + {:ok, status} = CommonAPI.post(user, %{status: "status #{i}"}) + CommonAPI.favorite(user, status.id) + Bookmark.create(user.id, status.id) + end) + + assert {:ok, backup} = user |> Backup.new() |> Repo.insert() + {:ok, backup} = Backup.process(backup) + + assert backup.processed_number == 1 + 120 + 120 + 120 + + Backup.delete(backup) + end + + test "it handles errors" do + user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"}) + + Enum.map(1..120, fn i -> + {:ok, _status} = CommonAPI.post(user, %{status: "status #{i}"}) + end) + + assert {:ok, backup} = user |> Backup.new() |> Repo.insert() + + with_mock Pleroma.Web.ActivityPub.Transmogrifier, + [:passthrough], + prepare_outgoing: fn data -> + object = + data["object"] + |> Pleroma.Object.normalize(fetch: false) + |> Map.get(:data) + + data = data |> Map.put("object", object) + + if String.contains?(data["object"]["content"], "119"), + do: raise(%Postgrex.Error{}), + else: {:ok, data} + end do + {:ok, backup} = Backup.process(backup) + assert backup.processed + assert backup.state == :complete + assert backup.processed_number == 1 + 119 + + Backup.delete(backup) + end + end + describe "it uploads and deletes a backup archive" do setup do clear_config([Pleroma.Upload, :base_url], "https://s3.amazonaws.com") @@ -209,7 +343,7 @@ defmodule Pleroma.User.BackupTest do Bookmark.create(user.id, status3.id) assert {:ok, backup} = user |> Backup.new() |> Repo.insert() - assert {:ok, path} = Backup.export(backup) + assert {:ok, path} = Backup.export(backup, self()) [path: path, backup: backup] end @@ -218,14 +352,14 @@ defmodule Pleroma.User.BackupTest do clear_config([Pleroma.Upload, :uploader], Pleroma.Uploaders.S3) clear_config([Pleroma.Uploaders.S3, :streaming_enabled], false) - with_mock ExAws, - request: fn - %{http_method: :put} -> {:ok, :ok} - %{http_method: :delete} -> {:ok, %{status_code: 204}} - end do - assert {:ok, %Pleroma.Upload{}} = Backup.upload(backup, path) - assert {:ok, _backup} = Backup.delete(backup) - end + ExAwsMock + |> expect(:request, 2, fn + %{http_method: :put} -> {:ok, :ok} + %{http_method: :delete} -> {:ok, %{status_code: 204}} + end) + + assert {:ok, %Pleroma.Upload{}} = Backup.upload(backup, path) + assert {:ok, _backup} = Backup.delete(backup) end test "Local", %{path: path, backup: backup} do diff --git a/test/pleroma/user/import_test.exs b/test/pleroma/user/import_test.exs index b4efd4bb0..f75305e0e 100644 --- a/test/pleroma/user/import_test.exs +++ b/test/pleroma/user/import_test.exs @@ -3,7 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.User.ImportTest do - alias Pleroma.Repo alias Pleroma.Tests.ObanHelpers alias Pleroma.User diff --git a/test/pleroma/user/query_test.exs b/test/pleroma/user/query_test.exs index bd45d1bca..30a4637f2 100644 --- a/test/pleroma/user/query_test.exs +++ b/test/pleroma/user/query_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.User.QueryTest do - use Pleroma.DataCase, async: true + use Pleroma.DataCase, async: false alias Pleroma.Repo alias Pleroma.User @@ -44,4 +44,63 @@ defmodule Pleroma.User.QueryTest do |> User.Query.build() |> Repo.all() end + + describe "is_privileged param" do + setup do + %{ + user: insert(:user, local: true, is_admin: false, is_moderator: false), + moderator_user: insert(:user, local: true, is_admin: false, is_moderator: true), + admin_user: insert(:user, local: true, is_admin: true, is_moderator: false), + admin_moderator_user: insert(:user, local: true, is_admin: true, is_moderator: true), + remote_user: insert(:user, local: false, is_admin: true, is_moderator: true), + non_active_user: + insert(:user, local: true, is_admin: true, is_moderator: true, is_active: false) + } + end + + test "doesn't return any users when there are no privileged roles" do + clear_config([:instance, :admin_privileges], []) + clear_config([:instance, :moderator_privileges], []) + + assert [] = User.Query.build(%{is_privileged: :cofe}) |> Repo.all() + end + + test "returns moderator users if they are privileged", %{ + moderator_user: moderator_user, + admin_moderator_user: admin_moderator_user + } do + clear_config([:instance, :admin_privileges], []) + clear_config([:instance, :moderator_privileges], [:cofe]) + + assert [_, _] = User.Query.build(%{is_privileged: :cofe}) |> Repo.all() + assert moderator_user in (User.Query.build(%{is_privileged: :cofe}) |> Repo.all()) + assert admin_moderator_user in (User.Query.build(%{is_privileged: :cofe}) |> Repo.all()) + end + + test "returns admin users if they are privileged", %{ + admin_user: admin_user, + admin_moderator_user: admin_moderator_user + } do + clear_config([:instance, :admin_privileges], [:cofe]) + clear_config([:instance, :moderator_privileges], []) + + assert [_, _] = User.Query.build(%{is_privileged: :cofe}) |> Repo.all() + assert admin_user in (User.Query.build(%{is_privileged: :cofe}) |> Repo.all()) + assert admin_moderator_user in (User.Query.build(%{is_privileged: :cofe}) |> Repo.all()) + end + + test "returns admin and moderator users if they are both privileged", %{ + moderator_user: moderator_user, + admin_user: admin_user, + admin_moderator_user: admin_moderator_user + } do + clear_config([:instance, :admin_privileges], [:cofe]) + clear_config([:instance, :moderator_privileges], [:cofe]) + + assert [_, _, _] = User.Query.build(%{is_privileged: :cofe}) |> Repo.all() + assert admin_user in (User.Query.build(%{is_privileged: :cofe}) |> Repo.all()) + assert moderator_user in (User.Query.build(%{is_privileged: :cofe}) |> Repo.all()) + assert admin_moderator_user in (User.Query.build(%{is_privileged: :cofe}) |> Repo.all()) + end + end end diff --git a/test/pleroma/user_relationship_test.exs b/test/pleroma/user_relationship_test.exs index 2811aff4c..7d205a746 100644 --- a/test/pleroma/user_relationship_test.exs +++ b/test/pleroma/user_relationship_test.exs @@ -5,8 +5,9 @@ defmodule Pleroma.UserRelationshipTest do alias Pleroma.UserRelationship - use Pleroma.DataCase, async: true + use Pleroma.DataCase, async: false + import Mock import Pleroma.Factory describe "*_exists?/2" do @@ -79,7 +80,12 @@ defmodule Pleroma.UserRelationshipTest do end test "if record already exists, returns it", %{users: [user1, user2]} do - user_block = UserRelationship.create_block(user1, user2) + user_block = + with_mock NaiveDateTime, [:passthrough], utc_now: fn -> ~N[2017-03-17 17:09:58] end do + {:ok, %{inserted_at: ~N[2017-03-17 17:09:58]}} = + UserRelationship.create_block(user1, user2) + end + assert user_block == UserRelationship.create_block(user1, user2) end end diff --git a/test/pleroma/user_search_test.exs b/test/pleroma/user_search_test.exs index 9b94f421d..1af9a1493 100644 --- a/test/pleroma/user_search_test.exs +++ b/test/pleroma/user_search_test.exs @@ -3,7 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.UserSearchTest do - alias Pleroma.Repo alias Pleroma.User use Pleroma.DataCase @@ -65,6 +64,14 @@ defmodule Pleroma.UserSearchTest do assert found_user.id == user.id end + test "excludes deactivated users from results" do + user = insert(:user, %{nickname: "john t1000"}) + insert(:user, %{is_active: false, nickname: "john t800"}) + + [found_user] = User.search("john") + assert found_user.id == user.id + end + # Note: as in Mastodon, `is_discoverable` doesn't anyhow relate to user searchability test "includes non-discoverable users in results" do insert(:user, %{nickname: "john 3000", is_discoverable: false}) diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index 884b846ae..5b7a65658 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -5,7 +5,6 @@ defmodule Pleroma.UserTest do alias Pleroma.Activity alias Pleroma.Builders.UserBuilder - alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo alias Pleroma.Tests.ObanHelpers @@ -13,13 +12,18 @@ defmodule Pleroma.UserTest do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI - use Pleroma.DataCase + use Pleroma.DataCase, async: false use Oban.Testing, repo: Pleroma.Repo import Pleroma.Factory import ExUnit.CaptureLog import Swoosh.TestAssertions + setup do + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + :ok + end + setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) :ok @@ -222,7 +226,7 @@ defmodule Pleroma.UserTest do assert [] = User.get_follow_requests(followed) end - test "follow_all follows mutliple users" do + test "follow_all follows multiple users" do user = insert(:user) followed_zero = insert(:user) followed_one = insert(:user) @@ -246,7 +250,7 @@ defmodule Pleroma.UserTest do refute User.following?(user, reverse_blocked) end - test "follow_all follows mutliple users without duplicating" do + test "follow_all follows multiple users without duplicating" do user = insert(:user) followed_zero = insert(:user) followed_one = insert(:user) @@ -311,7 +315,7 @@ defmodule Pleroma.UserTest do describe "unfollow/2" do setup do: clear_config([:instance, :external_user_synchronization]) - test "unfollow with syncronizes external user" do + test "unfollow with synchronizes external user" do clear_config([:instance, :external_user_synchronization], true) followed = @@ -473,12 +477,7 @@ defmodule Pleroma.UserTest do reject_deletes: [] ) - setup do: - clear_config(:mrf, - policies: [ - Pleroma.Web.ActivityPub.MRF.SimplePolicy - ] - ) + setup do: clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy]) test "it sends a welcome chat message when Simple policy applied to local instance" do clear_config([:mrf_simple, :media_nsfw], [{"localhost", ""}]) @@ -591,6 +590,21 @@ defmodule Pleroma.UserTest do refute_email_sent() end + test "it works when the registering user does not provide an email" do + clear_config([Pleroma.Emails.Mailer, :enabled], false) + clear_config([:instance, :account_activation_required], false) + clear_config([:instance, :account_approval_required], true) + + cng = User.register_changeset(%User{}, @full_user_data |> Map.put(:email, "")) + + # The user is still created + assert {:ok, %User{nickname: "nick"}} = User.register(cng) + + # No emails are sent + ObanHelpers.perform_all() + refute_email_sent() + end + test "it requires an email, name, nickname and password, bio is optional when account_activation_required is enabled" do clear_config([:instance, :account_activation_required], true) @@ -618,9 +632,10 @@ defmodule Pleroma.UserTest do end test "it restricts certain nicknames" do + clear_config([User, :restricted_nicknames], ["about"]) [restricted_name | _] = Pleroma.Config.get([User, :restricted_nicknames]) - assert is_bitstring(restricted_name) + assert is_binary(restricted_name) params = @full_user_data @@ -631,6 +646,23 @@ defmodule Pleroma.UserTest do refute changeset.valid? end + test "it is case-insensitive when restricting nicknames" do + clear_config([User, :restricted_nicknames], ["about"]) + [restricted_name | _] = Pleroma.Config.get([User, :restricted_nicknames]) + + assert is_binary(restricted_name) + + restricted_upcase_name = String.upcase(restricted_name) + + params = + @full_user_data + |> Map.put(:nickname, restricted_upcase_name) + + changeset = User.register_changeset(%User{}, params) + + refute changeset.valid? + end + test "it blocks blacklisted email domains" do clear_config([User, :email_blacklist], ["trolling.world"]) @@ -639,6 +671,11 @@ defmodule Pleroma.UserTest do changeset = User.register_changeset(%User{}, params) refute changeset.valid? + # Block with case-insensitive match + params = Map.put(@full_user_data, :email, "troll@TrOlLing.wOrld") + changeset = User.register_changeset(%User{}, params) + refute changeset.valid? + # Block with subdomain match params = Map.put(@full_user_data, :email, "troll@gnomes.trolling.world") changeset = User.register_changeset(%User{}, params) @@ -654,14 +691,14 @@ defmodule Pleroma.UserTest do assert changeset.valid? end - test "it sets the password_hash and ap_id" do + test "it sets the password_hash, ap_id, private key and followers collection address" do changeset = User.register_changeset(%User{}, @full_user_data) assert changeset.valid? assert is_binary(changeset.changes[:password_hash]) + assert is_binary(changeset.changes[:keys]) assert changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname}) - assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers" end @@ -827,6 +864,33 @@ defmodule Pleroma.UserTest do freshed_user = refresh_record(user) assert freshed_user == fetched_user end + + test "gets an existing user by nickname starting with http" do + user = insert(:user, nickname: "httpssome") + {:ok, fetched_user} = User.get_or_fetch("httpssome") + + assert user == fetched_user + end + end + + describe "get_or_fetch/1 remote users with tld, while BE is running on a subdomain" do + setup do: clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true) + + test "for mastodon" do + ap_id = "a@mastodon.example" + {:ok, fetched_user} = User.get_or_fetch(ap_id) + + assert fetched_user.ap_id == "https://sub.mastodon.example/users/a" + assert fetched_user.nickname == "a@mastodon.example" + end + + test "for pleroma" do + ap_id = "a@pleroma.example" + {:ok, fetched_user} = User.get_or_fetch(ap_id) + + assert fetched_user.ap_id == "https://sub.pleroma.example/users/a" + assert fetched_user.nickname == "a@pleroma.example" + end end describe "fetching a user from nickname or trying to build one" do @@ -864,13 +928,13 @@ defmodule Pleroma.UserTest do @tag capture_log: true test "returns nil if no user could be fetched" do - {:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la") - assert fetched_user == "not found nonexistant@social.heldscal.la" + {:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistent@social.heldscal.la") + assert fetched_user == "not found nonexistent@social.heldscal.la" end - test "returns nil for nonexistant local user" do - {:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant") - assert fetched_user == "not found nonexistant" + test "returns nil for nonexistent local user" do + {:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistent") + assert fetched_user == "not found nonexistent" end test "updates an existing user, if stale" do @@ -978,7 +1042,7 @@ defmodule Pleroma.UserTest do assert cs.valid? end - test "it sets the follower_adress" do + test "it sets the follower_address" do cs = User.remote_user_changeset(@valid_remote) # remote users get a fake local follower address assert cs.changes.follower_address == @@ -1123,7 +1187,7 @@ defmodule Pleroma.UserTest do user = insert(:user) muted_user = insert(:user) - {:ok, _user_relationships} = User.mute(user, muted_user, %{expires_in: 60}) + {:ok, _user_relationships} = User.mute(user, muted_user, %{duration: 60}) assert User.mutes?(user, muted_user) worker = Pleroma.Workers.MuteExpireWorker @@ -1695,7 +1759,6 @@ defmodule Pleroma.UserTest do confirmation_token: "qqqq", domain_blocks: ["lain.com"], is_active: false, - ap_enabled: true, is_moderator: true, is_admin: true, mascot: %{"a" => "b"}, @@ -1736,7 +1799,6 @@ defmodule Pleroma.UserTest do confirmation_token: nil, domain_blocks: [], is_active: false, - ap_enabled: false, is_moderator: false, is_admin: false, mascot: nil, @@ -1799,8 +1861,8 @@ defmodule Pleroma.UserTest do end end - test "get_public_key_for_ap_id fetches a user that's not in the db" do - assert {:ok, _key} = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin") + test "get_public_key_for_ap_id returns correctly for user that's not in the db" do + assert :error = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin") end describe "per-user rich-text filtering" do @@ -1883,31 +1945,82 @@ defmodule Pleroma.UserTest do end end - describe "superuser?/1" do + describe "privileged?/1" do + setup do + clear_config([:instance, :admin_privileges], [:cofe, :suya]) + clear_config([:instance, :moderator_privileges], [:cofe, :suya]) + end + test "returns false for unprivileged users" do user = insert(:user, local: true) - refute User.superuser?(user) + refute User.privileged?(user, :cofe) end test "returns false for remote users" do user = insert(:user, local: false) remote_admin_user = insert(:user, local: false, is_admin: true) - refute User.superuser?(user) - refute User.superuser?(remote_admin_user) + refute User.privileged?(user, :cofe) + refute User.privileged?(remote_admin_user, :cofe) end - test "returns true for local moderators" do + test "returns true for local moderators if, and only if, they are privileged" do user = insert(:user, local: true, is_moderator: true) - assert User.superuser?(user) + assert User.privileged?(user, :cofe) + + clear_config([:instance, :moderator_privileges], []) + + refute User.privileged?(user, :cofe) end - test "returns true for local admins" do + test "returns true for local admins if, and only if, they are privileged" do user = insert(:user, local: true, is_admin: true) - assert User.superuser?(user) + assert User.privileged?(user, :cofe) + + clear_config([:instance, :admin_privileges], []) + + refute User.privileged?(user, :cofe) + end + end + + describe "privileges/1" do + setup do + clear_config([:instance, :moderator_privileges], [:cofe, :only_moderator]) + clear_config([:instance, :admin_privileges], [:cofe, :only_admin]) + end + + test "returns empty list for users without roles" do + user = insert(:user, local: true) + + assert [] == User.privileges(user) + end + + test "returns list of privileges for moderators" do + moderator = insert(:user, is_moderator: true, local: true) + + assert [:cofe, :only_moderator] == User.privileges(moderator) |> Enum.sort() + end + + test "returns list of privileges for admins" do + admin = insert(:user, is_admin: true, local: true) + + assert [:cofe, :only_admin] == User.privileges(admin) |> Enum.sort() + end + + test "returns list of unique privileges for users who are both moderator and admin" do + moderator_admin = insert(:user, is_moderator: true, is_admin: true, local: true) + + assert [:cofe, :only_admin, :only_moderator] == + User.privileges(moderator_admin) |> Enum.sort() + end + + test "returns empty list for remote users" do + remote_moderator_admin = insert(:user, is_moderator: true, is_admin: true, local: false) + + assert [] == User.privileges(remote_moderator_admin) end end @@ -1950,13 +2063,77 @@ defmodule Pleroma.UserTest do assert User.visible_for(user, other_user) == :visible end - test "returns true when the account is unconfirmed and being viewed by a privileged account (confirmation required)" do + test "returns true when the account is unconfirmed and being viewed by a privileged account (privilege :users_manage_activation_state, confirmation required)" do clear_config([:instance, :account_activation_required], true) + clear_config([:instance, :admin_privileges], [:users_manage_activation_state]) user = insert(:user, local: true, is_confirmed: false) other_user = insert(:user, local: true, is_admin: true) assert User.visible_for(user, other_user) == :visible + + clear_config([:instance, :admin_privileges], []) + + refute User.visible_for(user, other_user) == :visible + end + end + + describe "all_users_with_privilege/1" do + setup do + %{ + user: insert(:user, local: true, is_admin: false, is_moderator: false), + moderator_user: insert(:user, local: true, is_admin: false, is_moderator: true), + admin_user: insert(:user, local: true, is_admin: true, is_moderator: false), + admin_moderator_user: insert(:user, local: true, is_admin: true, is_moderator: true), + remote_user: insert(:user, local: false, is_admin: true, is_moderator: true), + non_active_user: + insert(:user, local: true, is_admin: true, is_moderator: true, is_active: false) + } + end + + test "doesn't return any users when there are no privileged roles" do + clear_config([:instance, :admin_privileges], []) + clear_config([:instance, :moderator_privileges], []) + + assert [] = User.Query.build(%{is_privileged: :cofe}) |> Repo.all() + end + + test "returns moderator users if they are privileged", %{ + moderator_user: moderator_user, + admin_moderator_user: admin_moderator_user + } do + clear_config([:instance, :admin_privileges], []) + clear_config([:instance, :moderator_privileges], [:cofe]) + + assert [_, _] = User.Query.build(%{is_privileged: :cofe}) |> Repo.all() + assert moderator_user in User.all_users_with_privilege(:cofe) + assert admin_moderator_user in User.all_users_with_privilege(:cofe) + end + + test "returns admin users if they are privileged", %{ + admin_user: admin_user, + admin_moderator_user: admin_moderator_user + } do + clear_config([:instance, :admin_privileges], [:cofe]) + clear_config([:instance, :moderator_privileges], []) + + assert [_, _] = User.Query.build(%{is_privileged: :cofe}) |> Repo.all() + assert admin_user in User.all_users_with_privilege(:cofe) + assert admin_moderator_user in User.all_users_with_privilege(:cofe) + end + + test "returns admin and moderator users if they are both privileged", %{ + moderator_user: moderator_user, + admin_user: admin_user, + admin_moderator_user: admin_moderator_user + } do + clear_config([:instance, :admin_privileges], [:cofe]) + clear_config([:instance, :moderator_privileges], [:cofe]) + + assert [_, _, _] = User.Query.build(%{is_privileged: :cofe}) |> Repo.all() + assert admin_user in User.all_users_with_privilege(:cofe) + assert moderator_user in User.all_users_with_privilege(:cofe) + assert admin_moderator_user in User.all_users_with_privilege(:cofe) end end @@ -2108,21 +2285,6 @@ defmodule Pleroma.UserTest do end end - describe "ensure_keys_present" do - test "it creates keys for a user and stores them in info" do - user = insert(:user) - refute is_binary(user.keys) - {:ok, user} = User.ensure_keys_present(user) - assert is_binary(user.keys) - end - - test "it doesn't create keys if there already are some" do - user = insert(:user, keys: "xxx") - {:ok, user} = User.ensure_keys_present(user) - assert user.keys == "xxx" - end - end - describe "get_ap_ids_by_nicknames" do test "it returns a list of AP ids for a given set of nicknames" do user = insert(:user) @@ -2172,20 +2334,20 @@ defmodule Pleroma.UserTest do end end - describe "is_internal_user?/1" do + describe "internal?/1" do test "non-internal user returns false" do user = insert(:user) - refute User.is_internal_user?(user) + refute User.internal?(user) end test "user with no nickname returns true" do user = insert(:user, %{nickname: nil}) - assert User.is_internal_user?(user) + assert User.internal?(user) end test "user with internal-prefixed nickname returns true" do user = insert(:user, %{nickname: "internal.test"}) - assert User.is_internal_user?(user) + assert User.internal?(user) end end @@ -2211,26 +2373,6 @@ defmodule Pleroma.UserTest do assert {:ok, user} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") assert %User{bio: "test-bio"} = User.get_cached_by_ap_id(user.ap_id) end - - test "removes report notifs when user isn't superuser any more" do - report_activity = insert(:report_activity) - user = insert(:user, is_moderator: true, is_admin: true) - {:ok, _} = Notification.create_notifications(report_activity) - - assert [%Pleroma.Notification{type: "pleroma:report"}] = Notification.for_user(user) - - {:ok, user} = user |> User.admin_api_update(%{is_moderator: false}) - # is still superuser because still admin - assert [%Pleroma.Notification{type: "pleroma:report"}] = Notification.for_user(user) - - {:ok, user} = user |> User.admin_api_update(%{is_moderator: true, is_admin: false}) - # is still superuser because still moderator - assert [%Pleroma.Notification{type: "pleroma:report"}] = Notification.for_user(user) - - {:ok, user} = user |> User.admin_api_update(%{is_moderator: false}) - # is not a superuser any more - assert [] = Notification.for_user(user) - end end describe "following/followers synchronization" do @@ -2244,8 +2386,7 @@ defmodule Pleroma.UserTest do insert(:user, local: false, follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following", - ap_enabled: true + following_address: "http://localhost:4001/users/masto_closed/following" ) assert other_user.following_count == 0 @@ -2257,7 +2398,7 @@ defmodule Pleroma.UserTest do assert other_user.follower_count == 1 end - test "syncronizes the counters with the remote instance for the followed when enabled" do + test "synchronizes the counters with the remote instance for the followed when enabled" do clear_config([:instance, :external_user_synchronization], false) user = insert(:user) @@ -2266,8 +2407,7 @@ defmodule Pleroma.UserTest do insert(:user, local: false, follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following", - ap_enabled: true + following_address: "http://localhost:4001/users/masto_closed/following" ) assert other_user.following_count == 0 @@ -2279,7 +2419,7 @@ defmodule Pleroma.UserTest do assert other_user.follower_count == 437 end - test "syncronizes the counters with the remote instance for the follower when enabled" do + test "synchronizes the counters with the remote instance for the follower when enabled" do clear_config([:instance, :external_user_synchronization], false) user = insert(:user) @@ -2288,8 +2428,7 @@ defmodule Pleroma.UserTest do insert(:user, local: false, follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following", - ap_enabled: true + following_address: "http://localhost:4001/users/masto_closed/following" ) assert other_user.following_count == 0 @@ -2454,13 +2593,23 @@ defmodule Pleroma.UserTest do end describe "full_nickname/1" do - test "returns fully qualified nickname for local and remote users" do - local_user = - insert(:user, nickname: "local_user", ap_id: "https://somehost.com/users/local_user") + test "returns fully qualified nickname for local users" do + local_user = insert(:user, nickname: "local_user") + + assert User.full_nickname(local_user) == "local_user@localhost" + end + + test "returns fully qualified nickname for local users when using different domain for webfinger" do + clear_config([Pleroma.Web.WebFinger, :domain], "plemora.dev") + + local_user = insert(:user, nickname: "local_user") + assert User.full_nickname(local_user) == "local_user@plemora.dev" + end + + test "returns fully qualified nickname for remote users" do remote_user = insert(:user, nickname: "remote@host.com", local: false) - assert User.full_nickname(local_user) == "local_user@somehost.com" assert User.full_nickname(remote_user) == "remote@host.com" end @@ -2579,6 +2728,96 @@ defmodule Pleroma.UserTest do object_id end + describe "add_alias/2" do + test "should add alias for another user" do + user = insert(:user) + user2 = insert(:user) + + assert {:ok, user_updated} = user |> User.add_alias(user2) + + assert user_updated.also_known_as |> length() == 1 + assert user2.ap_id in user_updated.also_known_as + end + + test "should add multiple aliases" do + user = insert(:user) + user2 = insert(:user) + user3 = insert(:user) + + assert {:ok, user} = user |> User.add_alias(user2) + assert {:ok, user_updated} = user |> User.add_alias(user3) + + assert user_updated.also_known_as |> length() == 2 + assert user2.ap_id in user_updated.also_known_as + assert user3.ap_id in user_updated.also_known_as + end + + test "should not add duplicate aliases" do + user = insert(:user) + user2 = insert(:user) + + assert {:ok, user} = user |> User.add_alias(user2) + + assert {:ok, user_updated} = user |> User.add_alias(user2) + + assert user_updated.also_known_as |> length() == 1 + assert user2.ap_id in user_updated.also_known_as + end + end + + describe "alias_users/1" do + test "should get aliases for a user" do + user = insert(:user) + user2 = insert(:user, also_known_as: [user.ap_id]) + + aliases = user2 |> User.alias_users() + + assert aliases |> length() == 1 + + alias_user = aliases |> Enum.at(0) + + assert alias_user.ap_id == user.ap_id + end + end + + describe "delete_alias/2" do + test "should delete existing alias" do + user = insert(:user) + user2 = insert(:user, also_known_as: [user.ap_id]) + + assert {:ok, user_updated} = user2 |> User.delete_alias(user) + + assert user_updated.also_known_as == [] + end + + test "should report error on non-existing alias" do + user = insert(:user) + user2 = insert(:user) + user3 = insert(:user, also_known_as: [user.ap_id]) + + assert {:error, :no_such_alias} = user3 |> User.delete_alias(user2) + + user3_updated = User.get_cached_by_ap_id(user3.ap_id) + + assert user3_updated.also_known_as |> length() == 1 + assert user.ap_id in user3_updated.also_known_as + end + end + + describe "get_familiar_followers/3" do + test "returns familiar followers for a pair of users" do + user1 = insert(:user) + %{id: id2} = user2 = insert(:user) + user3 = insert(:user) + _user4 = insert(:user) + + User.follow(user1, user2) + User.follow(user2, user3) + + assert [%{id: ^id2}] = User.get_familiar_followers(user3, user1) + end + end + describe "account endorsements" do test "it pins people" do user = insert(:user) @@ -2613,4 +2852,51 @@ defmodule Pleroma.UserTest do refute User.endorses?(user, pinned_user) end end + + test "it checks fields links for a backlink" do + user = insert(:user, ap_id: "https://social.example.org/users/lain") + + fields = [ + %{"name" => "Link", "value" => "http://example.com/rel_me/null"}, + %{"name" => "Verified link", "value" => "http://example.com/rel_me/link"}, + %{"name" => "Not a link", "value" => "i'm not a link"} + ] + + user + |> User.update_and_set_cache(%{raw_fields: fields}) + + ObanHelpers.perform_all() + + user = User.get_cached_by_id(user.id) + + assert [ + %{"verified_at" => nil}, + %{"verified_at" => verified_at}, + %{"verified_at" => nil} + ] = user.fields + + assert is_binary(verified_at) + end + + test "updating fields does not invalidate previously validated links" do + user = insert(:user, ap_id: "https://social.example.org/users/lain") + + user + |> User.update_and_set_cache(%{ + raw_fields: [%{"name" => "verified link", "value" => "http://example.com/rel_me/link"}] + }) + + ObanHelpers.perform_all() + + %User{fields: [%{"verified_at" => verified_at}]} = user = User.get_cached_by_id(user.id) + + user + |> User.update_and_set_cache(%{ + raw_fields: [%{"name" => "Verified link", "value" => "http://example.com/rel_me/link"}] + }) + + user = User.get_cached_by_id(user.id) + + assert [%{"verified_at" => ^verified_at}] = user.fields + end end diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 1c5c40e84..ec4c04c62 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -25,6 +25,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do require Pleroma.Constants + setup do + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + :ok + end + setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) :ok @@ -216,7 +221,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do user = insert(:user) {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"}) - assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post) + assert Pleroma.Web.ActivityPub.Visibility.local_public?(post) object = Object.normalize(post, fetch: false) uuid = String.split(object.data["id"], "/") |> List.last() @@ -233,7 +238,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do user = insert(:user) {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"}) - assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post) + assert Pleroma.Web.ActivityPub.Visibility.local_public?(post) object = Object.normalize(post, fetch: false) uuid = String.split(object.data["id"], "/") |> List.last() @@ -247,6 +252,27 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert json_response(response, 200) == ObjectView.render("object.json", %{object: object}) end + test "does not return local-only objects for remote users", %{conn: conn} do + user = insert(:user) + reader = insert(:user, local: false) + + {:ok, post} = + CommonAPI.post(user, %{status: "test @#{reader.nickname}", visibility: "local"}) + + assert Pleroma.Web.ActivityPub.Visibility.local_public?(post) + + object = Object.normalize(post, fetch: false) + uuid = String.split(object.data["id"], "/") |> List.last() + + assert response = + conn + |> assign(:user, reader) + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}") + + json_response(response, 404) + end + test "it returns a json representation of the object with accept application/json", %{ conn: conn } do @@ -410,7 +436,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do user = insert(:user) {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"}) - assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post) + assert Pleroma.Web.ActivityPub.Visibility.local_public?(post) uuid = String.split(post.data["id"], "/") |> List.last() @@ -426,7 +452,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do user = insert(:user) {:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"}) - assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post) + assert Pleroma.Web.ActivityPub.Visibility.local_public?(post) uuid = String.split(post.data["id"], "/") |> List.last() @@ -554,7 +580,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do user = insert(:user, ap_id: "https://mastodon.example.org/users/raymoo", - ap_enabled: true, local: false, last_refreshed_at: nil ) @@ -870,6 +895,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert Activity.get_by_ap_id(data["id"]) end + test "it rejects an invalid incoming activity", %{conn: conn, data: data} do + user = insert(:user, is_active: false) + + data = + data + |> Map.put("bcc", [user.ap_id]) + |> Kernel.put_in(["object", "bcc"], [user.ap_id]) + + conn = + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{user.nickname}/inbox", data) + + assert "Invalid request." == json_response(conn, 400) + end + test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do user = insert(:user) @@ -1297,6 +1339,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert outbox_endpoint == result["id"] end + test "it returns a local note activity when authenticated as local user", %{conn: conn} do + user = insert(:user) + reader = insert(:user) + {:ok, note_activity} = CommonAPI.post(user, %{status: "mew mew", visibility: "local"}) + ap_id = note_activity.data["id"] + + resp = + conn + |> assign(:user, reader) + |> put_req_header("accept", "application/activity+json") + |> get("/users/#{user.nickname}/outbox?page=true") + |> json_response(200) + + assert %{"orderedItems" => [%{"id" => ^ap_id}]} = resp + end + + test "it does not return a local note activity when unauthenticated", %{conn: conn} do + user = insert(:user) + {:ok, _note_activity} = CommonAPI.post(user, %{status: "mew mew", visibility: "local"}) + + resp = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/users/#{user.nickname}/outbox?page=true") + |> json_response(200) + + assert %{"orderedItems" => []} = resp + end + test "it returns a note activity in a collection", %{conn: conn} do note_activity = insert(:note_activity) note_object = Object.normalize(note_activity, fetch: false) diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 8aa586f40..524294385 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do alias Pleroma.Config alias Pleroma.Notification alias Pleroma.Object + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils @@ -19,11 +20,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do import ExUnit.CaptureLog import Mock + import Mox import Pleroma.Factory import Tesla.Mock setup do mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + :ok end @@ -174,7 +180,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) assert user.ap_id == user_id assert user.nickname == "admin@mastodon.example.org" - assert user.ap_enabled assert user.follower_address == "http://mastodon.example.org/users/admin/followers" end @@ -554,7 +559,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert activity.data["ok"] == data["ok"] assert activity.data["id"] == given_id assert activity.data["context"] == "blabla" - assert activity.data["context_id"] end test "adds a context when none is there" do @@ -576,8 +580,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert is_binary(activity.data["context"]) assert is_binary(object.data["context"]) - assert activity.data["context_id"] - assert object.data["context_id"] end test "adds an id to a given object if it lacks one and is a note and inserts it to the object database" do @@ -774,6 +776,34 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id) assert object.data["repliesCount"] == 2 end + + test "increates quotes count", %{user: user} do + user2 = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "1", visibility: "public"}) + ap_id = activity.data["id"] + quote_data = %{status: "1", quote_id: activity.id} + + # public + {:ok, _} = CommonAPI.post(user2, Map.put(quote_data, :visibility, "public")) + assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert object.data["quotesCount"] == 1 + + # unlisted + {:ok, _} = CommonAPI.post(user2, Map.put(quote_data, :visibility, "unlisted")) + assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert object.data["quotesCount"] == 2 + + # private + {:ok, _} = CommonAPI.post(user2, Map.put(quote_data, :visibility, "private")) + assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert object.data["quotesCount"] == 2 + + # direct + {:ok, _} = CommonAPI.post(user2, Map.put(quote_data, :visibility, "direct")) + assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id) + assert object.data["quotesCount"] == 2 + end end describe "fetch activities for recipients" do @@ -998,7 +1028,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do refute repeat_activity in activities end - test "see your own posts even when they adress actors from blocked domains" do + test "see your own posts even when they address actors from blocked domains" do user = insert(:user) domain = "dogwhistle.zone" @@ -1345,6 +1375,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do %{test_file: test_file} end + test "strips / from filename", %{test_file: file} do + file = %Plug.Upload{file | filename: "../../../../../nested/bad.jpg"} + {:ok, %Object{} = object} = ActivityPub.upload(file) + [%{"href" => href}] = object.data["url"] + assert Regex.match?(~r"/bad.jpg$", href) + refute Regex.match?(~r"/nested/", href) + end + test "sets a description if given", %{test_file: file} do {:ok, %Object{} = object} = ActivityPub.upload(file, description: "a cool file") assert object.data["name"] == "a cool file" @@ -1507,6 +1545,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do reporter_ap_id = reporter.ap_id target_ap_id = target_account.ap_id activity_ap_id = activity.data["id"] + object_ap_id = activity.object.data["id"] activity_with_object = Activity.get_by_ap_id_with_object(activity_ap_id) @@ -1518,6 +1557,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do reported_activity: activity, content: content, activity_ap_id: activity_ap_id, + object_ap_id: object_ap_id, activity_with_object: activity_with_object, reporter_ap_id: reporter_ap_id, target_ap_id: target_ap_id @@ -1531,7 +1571,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do target_account: target_account, reported_activity: reported_activity, content: content, - activity_ap_id: activity_ap_id, + object_ap_id: object_ap_id, activity_with_object: activity_with_object, reporter_ap_id: reporter_ap_id, target_ap_id: target_ap_id @@ -1547,7 +1587,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do note_obj = %{ "type" => "Note", - "id" => activity_ap_id, + "id" => object_ap_id, "content" => content, "published" => activity_with_object.object.data["published"], "actor" => @@ -1571,6 +1611,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do context: context, target_account: target_account, reported_activity: reported_activity, + object_ap_id: object_ap_id, content: content }, Utils, @@ -1585,8 +1626,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do content: content }) - new_data = - put_in(activity.data, ["object"], [target_account.ap_id, reported_activity.data["id"]]) + new_data = put_in(activity.data, ["object"], [target_account.ap_id, object_ap_id]) assert_called(Utils.maybe_federate(%{activity | data: new_data})) end @@ -1612,7 +1652,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do }) assert Repo.aggregate(Activity, :count, :id) == 1 - assert Repo.aggregate(Object, :count, :id) == 2 + assert Repo.aggregate(Object, :count, :id) == 1 assert Repo.aggregate(Notification, :count, :id) == 0 end end @@ -1665,7 +1705,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do end describe "fetch_follow_information_for_user" do - test "syncronizes following/followers counters" do + test "synchronizes following/followers counters" do user = insert(:user, local: false, @@ -1836,9 +1876,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do "target" => ^new_ap_id, "type" => "Move" }, - local: true + local: true, + recipients: recipients } = activity + assert old_user.follower_address in recipients + params = %{ "op" => "move_following", "origin_id" => old_user.id, @@ -1869,6 +1912,42 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert {:error, "Target account must have the origin in `alsoKnownAs`"} = ActivityPub.move(old_user, new_user) end + + test "do not move remote user following relationships" do + %{ap_id: old_ap_id} = old_user = insert(:user) + %{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id]) + follower_remote = insert(:user, local: false) + + User.follow(follower_remote, old_user) + + assert User.following?(follower_remote, old_user) + + assert {:ok, activity} = ActivityPub.move(old_user, new_user) + + assert %Activity{ + actor: ^old_ap_id, + data: %{ + "actor" => ^old_ap_id, + "object" => ^old_ap_id, + "target" => ^new_ap_id, + "type" => "Move" + }, + local: true + } = activity + + params = %{ + "op" => "move_following", + "origin_id" => old_user.id, + "target_id" => new_user.id + } + + assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params) + + Pleroma.Workers.BackgroundWorker.perform(%Oban.Job{args: params}) + + assert User.following?(follower_remote, old_user) + refute User.following?(follower_remote, new_user) + end end test "doesn't retrieve replies activities with exclude_replies" do @@ -2607,4 +2686,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do {:ok, user} = ActivityPub.make_user_from_ap_id("https://princess.cat/users/mewmew") assert user.name == " " end + + @tag capture_log: true + test "pin_data_from_featured_collection will ignore unsupported values" do + assert %{} == + ActivityPub.pin_data_from_featured_collection(%{ + "type" => "OrderedCollection", + "first" => "https://social.example/users/alice/collections/featured?page=true" + }) + end end diff --git a/test/pleroma/web/activity_pub/builder_test.exs b/test/pleroma/web/activity_pub/builder_test.exs index eb175a1be..52058a0a3 100644 --- a/test/pleroma/web/activity_pub/builder_test.exs +++ b/test/pleroma/web/activity_pub/builder_test.exs @@ -44,5 +44,34 @@ defmodule Pleroma.Web.ActivityPub.BuilderTest do assert {:ok, ^expected, []} = Builder.note(draft) end + + test "quote post" do + user = insert(:user) + note = insert(:note) + + draft = %ActivityDraft{ + user: user, + context: "2hu", + content_html: "<h1>This is :moominmamma: note</h1>", + quote_post: note, + extra: %{} + } + + expected = %{ + "actor" => user.ap_id, + "attachment" => [], + "content" => "<h1>This is :moominmamma: note</h1>", + "context" => "2hu", + "sensitive" => false, + "type" => "Note", + "quoteUrl" => note.data["id"], + "cc" => [], + "summary" => nil, + "tag" => [], + "to" => [] + } + + assert {:ok, ^expected, []} = Builder.note(draft) + end end end diff --git a/test/pleroma/web/activity_pub/mrf/anti_link_spam_policy_test.exs b/test/pleroma/web/activity_pub/mrf/anti_link_spam_policy_test.exs index 8c7d4de5c..303d7ca1e 100644 --- a/test/pleroma/web/activity_pub/mrf/anti_link_spam_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/anti_link_spam_policy_test.exs @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do import Pleroma.Factory import ExUnit.CaptureLog + alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy @linkless_message %{ @@ -49,15 +50,39 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do assert user.note_count == 0 + message = %{ + "type" => "Create", + "actor" => user.ap_id, + "object" => %{ + "formerRepresentations" => %{ + "type" => "OrderedCollection", + "orderedItems" => [ + %{ + "content" => "<a href='https://example.com'>hi world!</a>" + } + ] + }, + "content" => "mew" + } + } + + {:reject, _} = MRF.filter_one(AntiLinkSpamPolicy, message) + end + + test "it allows posts with links for local users" do + user = insert(:user) + + assert user.note_count == 0 + message = @linkful_message |> Map.put("actor", user.ap_id) - {:reject, _} = AntiLinkSpamPolicy.filter(message) + {:ok, _message} = AntiLinkSpamPolicy.filter(message) end - test "it allows posts with links for local users" do - user = insert(:user) + test "it disallows posts with links in history" do + user = insert(:user, local: false) assert user.note_count == 0 @@ -65,7 +90,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicyTest do @linkful_message |> Map.put("actor", user.ap_id) - {:ok, _message} = AntiLinkSpamPolicy.filter(message) + {:reject, _} = AntiLinkSpamPolicy.filter(message) end end diff --git a/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs b/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs new file mode 100644 index 000000000..7350800f0 --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs @@ -0,0 +1,425 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicyTest do + use Pleroma.DataCase + + require Pleroma.Constants + + alias Pleroma.Web.ActivityPub.MRF + alias Pleroma.Web.ActivityPub.MRF.EmojiPolicy + + setup do: clear_config(:mrf_emoji) + + setup do + clear_config([:mrf_emoji], %{ + remove_url: [], + remove_shortcode: [], + federated_timeline_removal_url: [], + federated_timeline_removal_shortcode: [] + }) + end + + @emoji_tags [ + %{ + "icon" => %{ + "type" => "Image", + "url" => "https://example.org/emoji/biribiri/mikoto_smile2.png" + }, + "id" => "https://example.org/emoji/biribiri/mikoto_smile2.png", + "name" => ":mikoto_smile2:", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z" + }, + %{ + "icon" => %{ + "type" => "Image", + "url" => "https://example.org/emoji/biribiri/mikoto_smile3.png" + }, + "id" => "https://example.org/emoji/biribiri/mikoto_smile3.png", + "name" => ":mikoto_smile3:", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z" + }, + %{ + "icon" => %{ + "type" => "Image", + "url" => "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png" + }, + "id" => "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png", + "name" => ":nekomimi_girl_emoji_007:", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z" + }, + %{ + "icon" => %{ + "type" => "Image", + "url" => "https://example.org/test.png" + }, + "id" => "https://example.org/test.png", + "name" => ":test:", + "type" => "Emoji", + "updated" => "1970-01-01T00:00:00Z" + } + ] + + @misc_tags [%{"type" => "Placeholder"}] + + @user_data %{ + "type" => "Person", + "id" => "https://example.org/placeholder", + "name" => "lol", + "tag" => @emoji_tags ++ @misc_tags + } + + @status_data %{ + "type" => "Create", + "object" => %{ + "type" => "Note", + "id" => "https://example.org/placeholder", + "content" => "lol", + "tag" => @emoji_tags ++ @misc_tags, + "emoji" => %{ + "mikoto_smile2" => "https://example.org/emoji/biribiri/mikoto_smile2.png", + "mikoto_smile3" => "https://example.org/emoji/biribiri/mikoto_smile3.png", + "nekomimi_girl_emoji_007" => + "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png", + "test" => "https://example.org/test.png" + }, + "to" => ["https://example.org/self", Pleroma.Constants.as_public()], + "cc" => ["https://example.org/someone"] + }, + "to" => ["https://example.org/self", Pleroma.Constants.as_public()], + "cc" => ["https://example.org/someone"] + } + + @status_data_with_history %{ + "type" => "Create", + "object" => + @status_data["object"] + |> Map.merge(%{ + "formerRepresentations" => %{ + "type" => "OrderedCollection", + "orderedItems" => [@status_data["object"] |> Map.put("content", "older")], + "totalItems" => 1 + } + }), + "to" => ["https://example.org/self", Pleroma.Constants.as_public()], + "cc" => ["https://example.org/someone"] + } + + @emoji_react_data %{ + "type" => "EmojiReact", + "tag" => [@emoji_tags |> Enum.at(3)], + "object" => "https://example.org/someobject", + "to" => ["https://example.org/self"], + "cc" => ["https://example.org/someone"] + } + + @emoji_react_data_matching_regex %{ + "type" => "EmojiReact", + "tag" => [@emoji_tags |> Enum.at(1)], + "object" => "https://example.org/someobject", + "to" => ["https://example.org/self"], + "cc" => ["https://example.org/someone"] + } + + @emoji_react_data_matching_nothing %{ + "type" => "EmojiReact", + "tag" => [@emoji_tags |> Enum.at(2)], + "object" => "https://example.org/someobject", + "to" => ["https://example.org/self"], + "cc" => ["https://example.org/someone"] + } + + @emoji_react_data_unicode %{ + "type" => "EmojiReact", + "content" => "😍", + "object" => "https://example.org/someobject", + "to" => ["https://example.org/self"], + "cc" => ["https://example.org/someone"] + } + + describe "remove_url" do + setup do + clear_config([:mrf_emoji, :remove_url], [ + "https://example.org/test.png", + ~r{/biribiri/mikoto_smile[23]\.png}, + "nekomimi_girl_emoji" + ]) + + :ok + end + + test "processes user" do + {:ok, filtered} = MRF.filter_one(EmojiPolicy, @user_data) + + expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags + + assert %{"tag" => ^expected_tags} = filtered + end + + test "processes status" do + {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data) + + expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags + + expected_emoji = %{ + "nekomimi_girl_emoji_007" => + "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png" + } + + assert %{"object" => %{"tag" => ^expected_tags, "emoji" => ^expected_emoji}} = filtered + end + + test "processes status with history" do + {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data_with_history) + + expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags + + expected_emoji = %{ + "nekomimi_girl_emoji_007" => + "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png" + } + + assert %{ + "object" => %{ + "tag" => ^expected_tags, + "emoji" => ^expected_emoji, + "formerRepresentations" => %{"orderedItems" => [item]} + } + } = filtered + + assert %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} = item + end + + test "processes updates" do + {:ok, filtered} = + MRF.filter_one(EmojiPolicy, @status_data_with_history |> Map.put("type", "Update")) + + expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags + + expected_emoji = %{ + "nekomimi_girl_emoji_007" => + "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png" + } + + assert %{ + "object" => %{ + "tag" => ^expected_tags, + "emoji" => ^expected_emoji, + "formerRepresentations" => %{"orderedItems" => [item]} + } + } = filtered + + assert %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} = item + end + + test "processes EmojiReact" do + assert {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"} == + MRF.filter_one(EmojiPolicy, @emoji_react_data) + + assert {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"} == + MRF.filter_one(EmojiPolicy, @emoji_react_data_matching_regex) + + assert {:ok, @emoji_react_data_matching_nothing} == + MRF.filter_one(EmojiPolicy, @emoji_react_data_matching_nothing) + + assert {:ok, @emoji_react_data_unicode} == + MRF.filter_one(EmojiPolicy, @emoji_react_data_unicode) + end + end + + describe "remove_shortcode" do + setup do + clear_config([:mrf_emoji, :remove_shortcode], [ + "test", + ~r{mikoto_s}, + "nekomimi_girl_emoji" + ]) + + :ok + end + + test "processes user" do + {:ok, filtered} = MRF.filter_one(EmojiPolicy, @user_data) + + expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags + + assert %{"tag" => ^expected_tags} = filtered + end + + test "processes status" do + {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data) + + expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags + + expected_emoji = %{ + "nekomimi_girl_emoji_007" => + "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png" + } + + assert %{"object" => %{"tag" => ^expected_tags, "emoji" => ^expected_emoji}} = filtered + end + + test "processes status with history" do + {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data_with_history) + + expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags + + expected_emoji = %{ + "nekomimi_girl_emoji_007" => + "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png" + } + + assert %{ + "object" => %{ + "tag" => ^expected_tags, + "emoji" => ^expected_emoji, + "formerRepresentations" => %{"orderedItems" => [item]} + } + } = filtered + + assert %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} = item + end + + test "processes updates" do + {:ok, filtered} = + MRF.filter_one(EmojiPolicy, @status_data_with_history |> Map.put("type", "Update")) + + expected_tags = [@emoji_tags |> Enum.at(2)] ++ @misc_tags + + expected_emoji = %{ + "nekomimi_girl_emoji_007" => + "https://example.org/emoji/nekomimi_girl_emoji/nekomimi_girl_emoji_007.png" + } + + assert %{ + "object" => %{ + "tag" => ^expected_tags, + "emoji" => ^expected_emoji, + "formerRepresentations" => %{"orderedItems" => [item]} + } + } = filtered + + assert %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} = item + end + + test "processes EmojiReact" do + assert {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"} == + MRF.filter_one(EmojiPolicy, @emoji_react_data) + + assert {:reject, "[EmojiPolicy] Rejected for having disallowed emoji"} == + MRF.filter_one(EmojiPolicy, @emoji_react_data_matching_regex) + + assert {:ok, @emoji_react_data_matching_nothing} == + MRF.filter_one(EmojiPolicy, @emoji_react_data_matching_nothing) + + assert {:ok, @emoji_react_data_unicode} == + MRF.filter_one(EmojiPolicy, @emoji_react_data_unicode) + end + end + + describe "federated_timeline_removal_url" do + setup do + clear_config([:mrf_emoji, :federated_timeline_removal_url], [ + "https://example.org/test.png", + ~r{/biribiri/mikoto_smile[23]\.png}, + "nekomimi_girl_emoji" + ]) + + :ok + end + + test "processes status" do + {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data) + + expected_tags = @status_data["object"]["tag"] + expected_emoji = @status_data["object"]["emoji"] + + expected_to = ["https://example.org/self"] + expected_cc = [Pleroma.Constants.as_public(), "https://example.org/someone"] + + assert %{ + "to" => ^expected_to, + "cc" => ^expected_cc, + "object" => %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} + } = filtered + end + + test "ignore updates" do + {:ok, filtered} = MRF.filter_one(EmojiPolicy, @status_data |> Map.put("type", "Update")) + + expected_tags = @status_data["object"]["tag"] + expected_emoji = @status_data["object"]["emoji"] + + expected_to = ["https://example.org/self", Pleroma.Constants.as_public()] + expected_cc = ["https://example.org/someone"] + + assert %{ + "to" => ^expected_to, + "cc" => ^expected_cc, + "object" => %{"tag" => ^expected_tags, "emoji" => ^expected_emoji} + } = filtered + end + + test "processes status with history" do + status = + @status_data_with_history + |> put_in(["object", "tag"], @misc_tags) + |> put_in(["object", "emoji"], %{}) + + {:ok, filtered} = MRF.filter_one(EmojiPolicy, status) + + expected_tags = @status_data["object"]["tag"] + expected_emoji = @status_data["object"]["emoji"] + + expected_to = ["https://example.org/self"] + expected_cc = [Pleroma.Constants.as_public(), "https://example.org/someone"] + + assert %{ + "to" => ^expected_to, + "cc" => ^expected_cc, + "object" => %{ + "formerRepresentations" => %{ + "orderedItems" => [%{"tag" => ^expected_tags, "emoji" => ^expected_emoji}] + } + } + } = filtered + end + end + + describe "edge cases" do + setup do + clear_config([:mrf_emoji, :remove_url], [ + "https://example.org/test.png", + ~r{/biribiri/mikoto_smile[23]\.png}, + "nekomimi_girl_emoji" + ]) + + :ok + end + + test "non-statuses" do + answer = @status_data |> put_in(["object", "type"], "Answer") + {:ok, filtered} = MRF.filter_one(EmojiPolicy, answer) + + assert filtered == answer + end + + test "without tag" do + status = @status_data |> Map.put("object", Map.drop(@status_data["object"], ["tag"])) + {:ok, filtered} = MRF.filter_one(EmojiPolicy, status) + + refute Map.has_key?(filtered["object"], "tag") + end + + test "without emoji" do + status = @status_data |> Map.put("object", Map.drop(@status_data["object"], ["emoji"])) + {:ok, filtered} = MRF.filter_one(EmojiPolicy, status) + + refute Map.has_key?(filtered["object"], "emoji") + end + end +end diff --git a/test/pleroma/web/activity_pub/mrf/ensure_re_prepended_test.exs b/test/pleroma/web/activity_pub/mrf/ensure_re_prepended_test.exs index bc2f09e29..5afab0cf9 100644 --- a/test/pleroma/web/activity_pub/mrf/ensure_re_prepended_test.exs +++ b/test/pleroma/web/activity_pub/mrf/ensure_re_prepended_test.exs @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrependedTest do alias Pleroma.Activity alias Pleroma.Object + alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF.EnsureRePrepended describe "rewrites summary" do @@ -23,7 +24,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrependedTest do assert res["object"]["summary"] == "re: object-summary" end - test "it adds `re:` to summary object when child summary containts re-subject of parent summary " do + test "it adds `re:` to summary object when child summary contains re-subject of parent summary " do message = %{ "type" => "Create", "object" => %{ @@ -35,10 +36,58 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrependedTest do assert {:ok, res} = EnsureRePrepended.filter(message) assert res["object"]["summary"] == "re: object-summary" end + + test "it adds `re:` to history" do + message = %{ + "type" => "Create", + "object" => %{ + "summary" => "object-summary", + "inReplyTo" => %Activity{object: %Object{data: %{"summary" => "object-summary"}}}, + "formerRepresentations" => %{ + "orderedItems" => [ + %{ + "summary" => "object-summary", + "inReplyTo" => %Activity{object: %Object{data: %{"summary" => "object-summary"}}} + } + ] + } + } + } + + assert {:ok, res} = MRF.filter_one(EnsureRePrepended, message) + assert res["object"]["summary"] == "re: object-summary" + + assert Enum.at(res["object"]["formerRepresentations"]["orderedItems"], 0)["summary"] == + "re: object-summary" + end + + test "it accepts Updates" do + message = %{ + "type" => "Update", + "object" => %{ + "summary" => "object-summary", + "inReplyTo" => %Activity{object: %Object{data: %{"summary" => "object-summary"}}}, + "formerRepresentations" => %{ + "orderedItems" => [ + %{ + "summary" => "object-summary", + "inReplyTo" => %Activity{object: %Object{data: %{"summary" => "object-summary"}}} + } + ] + } + } + } + + assert {:ok, res} = MRF.filter_one(EnsureRePrepended, message) + assert res["object"]["summary"] == "re: object-summary" + + assert Enum.at(res["object"]["formerRepresentations"]["orderedItems"], 0)["summary"] == + "re: object-summary" + end end describe "skip filter" do - test "it skip if type isn't 'Create'" do + test "it skip if type isn't 'Create' or 'Update'" do message = %{ "type" => "Annotation", "object" => %{"summary" => "object-summary"} diff --git a/test/pleroma/web/activity_pub/mrf/follow_bot_policy_test.exs b/test/pleroma/web/activity_pub/mrf/follow_bot_policy_test.exs index 248190034..a70e3c1a2 100644 --- a/test/pleroma/web/activity_pub/mrf/follow_bot_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/follow_bot_policy_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.FollowBotPolicyTest do - use Pleroma.DataCase, async: true + use Pleroma.DataCase alias Pleroma.User alias Pleroma.Web.ActivityPub.MRF.FollowBotPolicy diff --git a/test/pleroma/web/activity_pub/mrf/force_mention_test.exs b/test/pleroma/web/activity_pub/mrf/force_mention_test.exs new file mode 100644 index 000000000..b026bab66 --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/force_mention_test.exs @@ -0,0 +1,73 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionTest do + use Pleroma.DataCase + require Pleroma.Constants + + alias Pleroma.Web.ActivityPub.MRF.ForceMention + + import Pleroma.Factory + + test "adds mention to a reply" do + lain = + insert(:user, ap_id: "https://lain.com/users/lain", nickname: "lain@lain.com", local: false) + + niobleoum = + insert(:user, + ap_id: "https://www.minds.com/api/activitypub/users/1198929502760083472", + nickname: "niobleoum@minds.com", + local: false + ) + + status = File.read!("test/fixtures/minds-pleroma-mentioned-post.json") |> Jason.decode!() + + status_activity = %{ + "type" => "Create", + "actor" => lain.ap_id, + "object" => status + } + + Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(status_activity) + + reply = File.read!("test/fixtures/minds-invalid-mention-post.json") |> Jason.decode!() + + reply_activity = %{ + "type" => "Create", + "actor" => niobleoum.ap_id, + "object" => reply + } + + {:ok, %{"object" => %{"tag" => tag}}} = ForceMention.filter(reply_activity) + + assert Enum.find(tag, fn %{"href" => href} -> href == lain.ap_id end) + end + + test "adds mention to a quote" do + user1 = insert(:user, ap_id: "https://misskey.io/users/83ssedkv53") + user2 = insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i") + + status = File.read!("test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json") |> Jason.decode!() + + status_activity = %{ + "type" => "Create", + "actor" => user1.ap_id, + "object" => status + } + + Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(status_activity) + + quote_post = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!() + + quote_activity = %{ + "type" => "Create", + "actor" => user2.ap_id, + "object" => quote_post + } + + {:ok, %{"object" => %{"tag" => tag}}} = ForceMention.filter(quote_activity) + + assert Enum.find(tag, fn %{"href" => href} -> href == user1.ap_id end) + end +end diff --git a/test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs b/test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs index 125b14a59..811ef105c 100644 --- a/test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs +++ b/test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs @@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContentTest do alias Pleroma.Constants alias Pleroma.Object + alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent alias Pleroma.Web.CommonAPI @@ -161,4 +162,149 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContentTest do assert filtered == "<p><span class=\"recipients-inline\"><span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{luigi.id}\" href=\"#{luigi.ap_id}\" rel=\"ugc\">@<span>luigi</span></a></span> </span>I'ma tired...</p>" end + + test "aware of history" do + mario = insert(:user, nickname: "mario") + wario = insert(:user, nickname: "wario") + + {:ok, post1} = CommonAPI.post(mario, %{status: "Letsa go!"}) + + activity = %{ + "type" => "Create", + "actor" => wario.ap_id, + "object" => %{ + "type" => "Note", + "actor" => wario.ap_id, + "content" => "WHA-HA!", + "to" => [ + mario.ap_id, + Constants.as_public() + ], + "inReplyTo" => post1.object.data["id"], + "formerRepresentations" => %{ + "orderedItems" => [ + %{ + "type" => "Note", + "actor" => wario.ap_id, + "content" => "WHA-HA!", + "to" => [ + mario.ap_id, + Constants.as_public() + ], + "inReplyTo" => post1.object.data["id"] + } + ] + } + } + } + + expected = + "<span class=\"recipients-inline\"><span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{mario.id}\" href=\"#{mario.ap_id}\" rel=\"ugc\">@<span>mario</span></a></span> </span>WHA-HA!" + + assert {:ok, + %{ + "object" => %{ + "content" => ^expected, + "formerRepresentations" => %{"orderedItems" => [%{"content" => ^expected}]} + } + }} = MRF.filter_one(ForceMentionsInContent, activity) + end + + test "works with Updates" do + mario = insert(:user, nickname: "mario") + wario = insert(:user, nickname: "wario") + + {:ok, post1} = CommonAPI.post(mario, %{status: "Letsa go!"}) + + activity = %{ + "type" => "Update", + "actor" => wario.ap_id, + "object" => %{ + "type" => "Note", + "actor" => wario.ap_id, + "content" => "WHA-HA!", + "to" => [ + mario.ap_id, + Constants.as_public() + ], + "inReplyTo" => post1.object.data["id"], + "formerRepresentations" => %{ + "orderedItems" => [ + %{ + "type" => "Note", + "actor" => wario.ap_id, + "content" => "WHA-HA!", + "to" => [ + mario.ap_id, + Constants.as_public() + ], + "inReplyTo" => post1.object.data["id"] + } + ] + } + } + } + + expected = + "<span class=\"recipients-inline\"><span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{mario.id}\" href=\"#{mario.ap_id}\" rel=\"ugc\">@<span>mario</span></a></span> </span>WHA-HA!" + + assert {:ok, + %{ + "object" => %{ + "content" => ^expected, + "formerRepresentations" => %{"orderedItems" => [%{"content" => ^expected}]} + } + }} = MRF.filter_one(ForceMentionsInContent, activity) + end + + test "don't add duplicate mentions for mastodon or misskey posts" do + [zero, rogerick, greg] = [ + insert(:user, + ap_id: "https://pleroma.example.com/users/zero", + uri: "https://pleroma.example.com/users/zero", + nickname: "zero@pleroma.example.com", + local: false + ), + insert(:user, + ap_id: "https://misskey.example.com/users/104ab42f11", + uri: "https://misskey.example.com/@rogerick", + nickname: "rogerick@misskey.example.com", + local: false + ), + insert(:user, + ap_id: "https://mastodon.example.com/users/greg", + uri: "https://mastodon.example.com/@greg", + nickname: "greg@mastodon.example.com", + local: false + ) + ] + + {:ok, post} = CommonAPI.post(rogerick, %{status: "eugh"}) + + inline_mentions = [ + "<span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{rogerick.id}\" href=\"#{rogerick.ap_id}\" rel=\"ugc\">@<span>rogerick</span></a></span>", + "<span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{greg.id}\" href=\"#{greg.uri}\" rel=\"ugc\">@<span>greg</span></a></span>" + ] + + activity = %{ + "type" => "Create", + "actor" => zero.ap_id, + "object" => %{ + "type" => "Note", + "actor" => zero.ap_id, + "content" => "#{Enum.at(inline_mentions, 0)} #{Enum.at(inline_mentions, 1)} erm", + "to" => [ + rogerick.ap_id, + greg.ap_id, + Constants.as_public() + ], + "inReplyTo" => Object.normalize(post).data["id"] + } + } + + {:ok, %{"object" => %{"content" => filtered}}} = ForceMentionsInContent.filter(activity) + + assert filtered == + "#{Enum.at(inline_mentions, 0)} #{Enum.at(inline_mentions, 1)} erm" + end end diff --git a/test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs b/test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs index 7f2d78a4c..32991c966 100644 --- a/test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs @@ -20,6 +20,76 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicyTest do assert modified["object"]["sensitive"] end + test "it is history-aware" do + activity = %{ + "type" => "Create", + "object" => %{ + "content" => "hey", + "tag" => [] + } + } + + activity_data = + activity + |> put_in( + ["object", "formerRepresentations"], + %{ + "type" => "OrderedCollection", + "orderedItems" => [ + Map.put( + activity["object"], + "tag", + [%{"type" => "Hashtag", "name" => "#nsfw"}] + ) + ] + } + ) + + {:ok, modified} = + Pleroma.Web.ActivityPub.MRF.filter_one( + Pleroma.Web.ActivityPub.MRF.HashtagPolicy, + activity_data + ) + + refute modified["object"]["sensitive"] + assert Enum.at(modified["object"]["formerRepresentations"]["orderedItems"], 0)["sensitive"] + end + + test "it works with Update" do + activity = %{ + "type" => "Update", + "object" => %{ + "content" => "hey", + "tag" => [] + } + } + + activity_data = + activity + |> put_in( + ["object", "formerRepresentations"], + %{ + "type" => "OrderedCollection", + "orderedItems" => [ + Map.put( + activity["object"], + "tag", + [%{"type" => "Hashtag", "name" => "#nsfw"}] + ) + ] + } + ) + + {:ok, modified} = + Pleroma.Web.ActivityPub.MRF.filter_one( + Pleroma.Web.ActivityPub.MRF.HashtagPolicy, + activity_data + ) + + refute modified["object"]["sensitive"] + assert Enum.at(modified["object"]["formerRepresentations"]["orderedItems"], 0)["sensitive"] + end + test "it doesn't sets the sensitive property with irrelevant hashtags" do user = insert(:user) diff --git a/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs new file mode 100644 index 000000000..d5762766f --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs @@ -0,0 +1,112 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicyTest do + alias Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy + use Pleroma.DataCase + + test "adds quote URL to post content" do + quote_url = "https://gleasonator.com/objects/1234" + + activity = %{ + "type" => "Create", + "actor" => "https://gleasonator.com/users/alex", + "object" => %{ + "type" => "Note", + "content" => "Nice post", + "quoteUrl" => quote_url + } + } + + {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity) + + assert filtered == + "Nice post<span class=\"quote-inline\"><br/><br/><bdi>RT:</bdi> <a href=\"https://gleasonator.com/objects/1234\">https://gleasonator.com/objects/1234</a></span>" + end + + test "adds quote URL to post content, custom template" do + clear_config([:mrf_inline_quote, :template], "{url}'s quoting") + quote_url = "https://gleasonator.com/objects/1234" + + activity = %{ + "type" => "Create", + "actor" => "https://gleasonator.com/users/alex", + "object" => %{ + "type" => "Note", + "content" => "Nice post", + "quoteUrl" => quote_url + } + } + + {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity) + + assert filtered == + "Nice post<span class=\"quote-inline\"><br/><br/><a href=\"https://gleasonator.com/objects/1234\">https://gleasonator.com/objects/1234</a>'s quoting</span>" + end + + test "doesn't add line breaks to markdown posts" do + quote_url = "https://gleasonator.com/objects/1234" + + activity = %{ + "type" => "Create", + "actor" => "https://gleasonator.com/users/alex", + "object" => %{ + "type" => "Note", + "content" => "<p>Nice post</p>", + "quoteUrl" => quote_url + } + } + + {:ok, %{"object" => %{"content" => filtered}}} = InlineQuotePolicy.filter(activity) + + assert filtered == + "<p>Nice post<span class=\"quote-inline\"><br/><br/><bdi>RT:</bdi> <a href=\"https://gleasonator.com/objects/1234\">https://gleasonator.com/objects/1234</a></span></p>" + end + + test "ignores Misskey quote posts" do + object = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!() + + activity = %{ + "type" => "Create", + "actor" => "https://misskey.io/users/7rkrarq81i", + "object" => object + } + + {:ok, filtered} = InlineQuotePolicy.filter(activity) + assert filtered == activity + end + + test "ignores Fedibird quote posts" do + object = File.read!("test/fixtures/quote_post/fedibird_quote_post.json") |> Jason.decode!() + + # Normally the ObjectValidator will fix this before it reaches MRF + object = Map.put(object, "quoteUrl", object["quoteURL"]) + + activity = %{ + "type" => "Create", + "actor" => "https://fedibird.com/users/noellabo", + "object" => object + } + + {:ok, filtered} = InlineQuotePolicy.filter(activity) + assert filtered == activity + end + + test "skips objects which already have an .inline-quote span" do + object = + File.read!("test/fixtures/quote_post/fedibird_quote_mismatched.json") |> Jason.decode!() + + # Normally the ObjectValidator will fix this before it reaches MRF + object = Map.put(object, "quoteUrl", object["quoteUri"]) + + activity = %{ + "type" => "Create", + "actor" => "https://fedibird.com/users/noellabo", + "object" => object + } + + {:ok, filtered} = InlineQuotePolicy.filter(activity) + assert filtered == activity + end +end diff --git a/test/pleroma/web/activity_pub/mrf/keyword_policy_test.exs b/test/pleroma/web/activity_pub/mrf/keyword_policy_test.exs index bfa8e8f59..a0e77d7b9 100644 --- a/test/pleroma/web/activity_pub/mrf/keyword_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/keyword_policy_test.exs @@ -79,6 +79,54 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicyTest do KeywordPolicy.filter(message) end) end + + test "rejects if string matches in history" do + clear_config([:mrf_keyword, :reject], ["pun"]) + + message = %{ + "type" => "Create", + "object" => %{ + "content" => "just a daily reminder that compLAINer is a good", + "summary" => "", + "formerRepresentations" => %{ + "type" => "OrderedCollection", + "orderedItems" => [ + %{ + "content" => "just a daily reminder that compLAINer is a good pun", + "summary" => "" + } + ] + } + } + } + + assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} = + KeywordPolicy.filter(message) + end + + test "rejects Updates" do + clear_config([:mrf_keyword, :reject], ["pun"]) + + message = %{ + "type" => "Update", + "object" => %{ + "content" => "just a daily reminder that compLAINer is a good", + "summary" => "", + "formerRepresentations" => %{ + "type" => "OrderedCollection", + "orderedItems" => [ + %{ + "content" => "just a daily reminder that compLAINer is a good pun", + "summary" => "" + } + ] + } + } + } + + assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} = + KeywordPolicy.filter(message) + end end describe "delisting from ftl based on keywords" do @@ -157,6 +205,31 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicyTest do not (["https://www.w3.org/ns/activitystreams#Public"] == result["to"]) end) end + + test "delists if string matches in history" do + clear_config([:mrf_keyword, :federated_timeline_removal], ["pun"]) + + message = %{ + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "type" => "Create", + "object" => %{ + "content" => "just a daily reminder that compLAINer is a good", + "summary" => "", + "formerRepresentations" => %{ + "orderedItems" => [ + %{ + "content" => "just a daily reminder that compLAINer is a good pun", + "summary" => "" + } + ] + } + } + } + + {:ok, result} = KeywordPolicy.filter(message) + assert ["https://www.w3.org/ns/activitystreams#Public"] == result["cc"] + refute ["https://www.w3.org/ns/activitystreams#Public"] == result["to"] + end end describe "replacing keywords" do @@ -221,5 +294,63 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicyTest do result == "ZFS is free software" end) end + + test "replaces keyword if string matches in history" do + clear_config([:mrf_keyword, :replace], [{"opensource", "free software"}]) + + message = %{ + "type" => "Create", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => %{ + "content" => "ZFS is opensource", + "summary" => "", + "formerRepresentations" => %{ + "type" => "OrderedCollection", + "orderedItems" => [ + %{"content" => "ZFS is opensource mew mew", "summary" => ""} + ] + } + } + } + + {:ok, + %{ + "object" => %{ + "content" => "ZFS is free software", + "formerRepresentations" => %{ + "orderedItems" => [%{"content" => "ZFS is free software mew mew"}] + } + } + }} = KeywordPolicy.filter(message) + end + + test "replaces keyword in Updates" do + clear_config([:mrf_keyword, :replace], [{"opensource", "free software"}]) + + message = %{ + "type" => "Update", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "object" => %{ + "content" => "ZFS is opensource", + "summary" => "", + "formerRepresentations" => %{ + "type" => "OrderedCollection", + "orderedItems" => [ + %{"content" => "ZFS is opensource mew mew", "summary" => ""} + ] + } + } + } + + {:ok, + %{ + "object" => %{ + "content" => "ZFS is free software", + "formerRepresentations" => %{ + "orderedItems" => [%{"content" => "ZFS is free software mew mew"}] + } + } + }} = KeywordPolicy.filter(message) + end end end diff --git a/test/pleroma/web/activity_pub/mrf/media_proxy_warming_policy_test.exs b/test/pleroma/web/activity_pub/mrf/media_proxy_warming_policy_test.exs index 09301c6ca..0da3afa3b 100644 --- a/test/pleroma/web/activity_pub/mrf/media_proxy_warming_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/media_proxy_warming_policy_test.exs @@ -7,9 +7,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do use Pleroma.Tests.Helpers alias Pleroma.HTTP + alias Pleroma.UnstubbedConfigMock, as: ConfigMock + alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy import Mock + import Mox @message %{ "type" => "Create", @@ -22,6 +25,32 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do } } + @message_with_history %{ + "type" => "Create", + "object" => %{ + "type" => "Note", + "content" => "content", + "formerRepresentations" => %{ + "orderedItems" => [ + %{ + "type" => "Note", + "content" => "content", + "attachment" => [ + %{"url" => [%{"href" => "http://example.com/image.jpg"}]} + ] + } + ] + } + } + } + + setup do + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + + :ok + end + setup do: clear_config([:media_proxy, :enabled], true) test "it prefetches media proxy URIs" do @@ -50,4 +79,28 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicyTest do refute called(HTTP.get(:_, :_, :_)) end end + + test "history-aware" do + Tesla.Mock.mock(fn %{method: :get, url: "http://example.com/image.jpg"} -> + {:ok, %Tesla.Env{status: 200, body: ""}} + end) + + with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do + MRF.filter_one(MediaProxyWarmingPolicy, @message_with_history) + + assert called(HTTP.get(:_, :_, :_)) + end + end + + test "works with Updates" do + Tesla.Mock.mock(fn %{method: :get, url: "http://example.com/image.jpg"} -> + {:ok, %Tesla.Env{status: 200, body: ""}} + end) + + with_mock HTTP, get: fn _, _, _ -> {:ok, []} end do + MRF.filter_one(MediaProxyWarmingPolicy, @message_with_history |> Map.put("type", "Update")) + + assert called(HTTP.get(:_, :_, :_)) + end + end end diff --git a/test/pleroma/web/activity_pub/mrf/no_empty_policy_test.exs b/test/pleroma/web/activity_pub/mrf/no_empty_policy_test.exs index fe4bb8f0a..386ed395f 100644 --- a/test/pleroma/web/activity_pub/mrf/no_empty_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/no_empty_policy_test.exs @@ -151,4 +151,27 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicyTest do assert NoEmptyPolicy.filter(message) == {:reject, "[NoEmptyPolicy]"} end + + test "works with Update" do + message = %{ + "actor" => "http://localhost:4001/users/testuser", + "cc" => ["http://localhost:4001/users/testuser/followers"], + "object" => %{ + "actor" => "http://localhost:4001/users/testuser", + "attachment" => [], + "cc" => ["http://localhost:4001/users/testuser/followers"], + "source" => "", + "to" => [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "type" => "Note" + }, + "to" => [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "type" => "Update" + } + + assert NoEmptyPolicy.filter(message) == {:reject, "[NoEmptyPolicy]"} + end end diff --git a/test/pleroma/web/activity_pub/mrf/no_placeholder_text_policy_test.exs b/test/pleroma/web/activity_pub/mrf/no_placeholder_text_policy_test.exs index acc7c3566..3533c2bc8 100644 --- a/test/pleroma/web/activity_pub/mrf/no_placeholder_text_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/no_placeholder_text_policy_test.exs @@ -4,6 +4,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicyTest do use Pleroma.DataCase, async: true + alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy test "it clears content object" do @@ -20,6 +21,46 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicyTest do assert res["object"]["content"] == "" end + test "history-aware" do + message = %{ + "type" => "Create", + "object" => %{ + "content" => ".", + "attachment" => "image", + "formerRepresentations" => %{ + "orderedItems" => [%{"content" => ".", "attachment" => "image"}] + } + } + } + + assert {:ok, res} = MRF.filter_one(NoPlaceholderTextPolicy, message) + + assert %{ + "content" => "", + "formerRepresentations" => %{"orderedItems" => [%{"content" => ""}]} + } = res["object"] + end + + test "works with Updates" do + message = %{ + "type" => "Update", + "object" => %{ + "content" => ".", + "attachment" => "image", + "formerRepresentations" => %{ + "orderedItems" => [%{"content" => ".", "attachment" => "image"}] + } + } + } + + assert {:ok, res} = MRF.filter_one(NoPlaceholderTextPolicy, message) + + assert %{ + "content" => "", + "formerRepresentations" => %{"orderedItems" => [%{"content" => ""}]} + } = res["object"] + end + @messages [ %{ "type" => "Create", diff --git a/test/pleroma/web/activity_pub/mrf/normalize_markup_test.exs b/test/pleroma/web/activity_pub/mrf/normalize_markup_test.exs index 20176b63b..66a8f4e44 100644 --- a/test/pleroma/web/activity_pub/mrf/normalize_markup_test.exs +++ b/test/pleroma/web/activity_pub/mrf/normalize_markup_test.exs @@ -4,6 +4,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkupTest do use Pleroma.DataCase, async: true + alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF.NormalizeMarkup @html_sample """ @@ -16,24 +17,58 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkupTest do <script>alert('hacked')</script> """ - test "it filter html tags" do - expected = """ - <b>this is in bold</b> - <p>this is a paragraph</p> - this is a linebreak<br/> - this is a link with allowed "rel" attribute: <a href="http://example.com/" rel="tag">example.com</a> - this is a link with not allowed "rel" attribute: <a href="http://example.com/">example.com</a> - this is an image: <img src="http://example.com/image.jpg"/><br/> - alert('hacked') - """ + @expected """ + <b>this is in bold</b> + <p>this is a paragraph</p> + this is a linebreak<br/> + this is a link with allowed "rel" attribute: <a href="http://example.com/" rel="tag">example.com</a> + this is a link with not allowed "rel" attribute: <a href="http://example.com/">example.com</a> + this is an image: <img src="http://example.com/image.jpg"/><br/> + alert('hacked') + """ + test "it filter html tags" do message = %{"type" => "Create", "object" => %{"content" => @html_sample}} assert {:ok, res} = NormalizeMarkup.filter(message) - assert res["object"]["content"] == expected + assert res["object"]["content"] == @expected + end + + test "history-aware" do + message = %{ + "type" => "Create", + "object" => %{ + "content" => @html_sample, + "formerRepresentations" => %{"orderedItems" => [%{"content" => @html_sample}]} + } + } + + assert {:ok, res} = MRF.filter_one(NormalizeMarkup, message) + + assert %{ + "content" => @expected, + "formerRepresentations" => %{"orderedItems" => [%{"content" => @expected}]} + } = res["object"] + end + + test "works with Updates" do + message = %{ + "type" => "Update", + "object" => %{ + "content" => @html_sample, + "formerRepresentations" => %{"orderedItems" => [%{"content" => @html_sample}]} + } + } + + assert {:ok, res} = MRF.filter_one(NormalizeMarkup, message) + + assert %{ + "content" => @expected, + "formerRepresentations" => %{"orderedItems" => [%{"content" => @expected}]} + } = res["object"] end - test "it skips filter if type isn't `Create`" do + test "it skips filter if type isn't `Create` or `Update`" do message = %{"type" => "Note", "object" => %{}} assert {:ok, res} = NormalizeMarkup.filter(message) diff --git a/test/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy_test.exs b/test/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy_test.exs new file mode 100644 index 000000000..96b49b6a0 --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy_test.exs @@ -0,0 +1,73 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicyTest do + alias Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy + + use Pleroma.DataCase + + require Pleroma.Constants + + test "Add quote url to Link tag" do + quote_url = "https://gleasonator.com/objects/1234" + + activity = %{ + "type" => "Create", + "actor" => "https://gleasonator.com/users/alex", + "object" => %{ + "type" => "Note", + "content" => "Nice post", + "quoteUrl" => quote_url + } + } + + {:ok, %{"object" => object}} = QuoteToLinkTagPolicy.filter(activity) + + assert object["tag"] == [ + %{ + "type" => "Link", + "href" => quote_url, + "mediaType" => Pleroma.Constants.activity_json_canonical_mime_type() + } + ] + end + + test "Add quote url to Link tag, append to the end" do + quote_url = "https://gleasonator.com/objects/1234" + + activity = %{ + "type" => "Create", + "actor" => "https://gleasonator.com/users/alex", + "object" => %{ + "type" => "Note", + "content" => "Nice post", + "quoteUrl" => quote_url, + "tag" => [%{"type" => "Hashtag", "name" => "#foo"}] + } + } + + {:ok, %{"object" => object}} = QuoteToLinkTagPolicy.filter(activity) + + assert [_, tag] = object["tag"] + + assert tag == %{ + "type" => "Link", + "href" => quote_url, + "mediaType" => Pleroma.Constants.activity_json_canonical_mime_type() + } + end + + test "Bypass posts without quoteUrl" do + activity = %{ + "type" => "Create", + "actor" => "https://gleasonator.com/users/alex", + "object" => %{ + "type" => "Note", + "content" => "Nice post" + } + } + + assert {:ok, ^activity} = QuoteToLinkTagPolicy.filter(activity) + end +end diff --git a/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs b/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs index 674b506c3..57fc00af5 100644 --- a/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/simple_policy_test.exs @@ -57,6 +57,16 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do assert SimplePolicy.filter(local_message) == {:ok, local_message} end + + test "works with Updates" do + clear_config([:mrf_simple, :media_removal], [{"remote.instance", "Some reason"}]) + media_message = build_media_message(type: "Update") + + assert SimplePolicy.filter(media_message) == + {:ok, + media_message + |> Map.put("object", Map.delete(media_message["object"], "attachment"))} + end end describe "when :media_nsfw" do @@ -90,12 +100,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do assert SimplePolicy.filter(local_message) == {:ok, local_message} end + + test "works with Updates" do + clear_config([:mrf_simple, :media_nsfw], [{"remote.instance", "Whetever"}]) + media_message = build_media_message(type: "Update") + + assert SimplePolicy.filter(media_message) == + {:ok, put_in(media_message, ["object", "sensitive"], true)} + end end - defp build_media_message do + defp build_media_message(opts \\ []) do %{ "actor" => "https://remote.instance/users/bob", - "type" => "Create", + "type" => opts[:type] || "Create", "object" => %{ "attachment" => [%{}], "tag" => ["foo"], diff --git a/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs b/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs index 89d32352f..2c7497da5 100644 --- a/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs @@ -60,6 +60,59 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do |> File.exists?() end + test "works with unknown extension", %{path: path} do + message = %{ + "type" => "Create", + "object" => %{ + "emoji" => [{"firedfox", "https://example.org/emoji/firedfox"}], + "actor" => "https://example.org/users/admin" + } + } + + fullpath = Path.join(path, "firedfox.png") + + Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")} + end) + + clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468) + + refute "firedfox" in installed() + refute File.exists?(path) + + assert {:ok, _message} = StealEmojiPolicy.filter(message) + + assert "firedfox" in installed() + assert File.exists?(path) + assert File.exists?(fullpath) + end + + test "rejects invalid shortcodes", %{path: path} do + message = %{ + "type" => "Create", + "object" => %{ + "emoji" => [{"fired/fox", "https://example.org/emoji/firedfox"}], + "actor" => "https://example.org/users/admin" + } + } + + fullpath = Path.join(path, "fired/fox.png") + + Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox"} -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")} + end) + + clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468) + + refute "firedfox" in installed() + refute File.exists?(path) + + assert {:ok, _message} = StealEmojiPolicy.filter(message) + + refute "fired/fox" in installed() + refute File.exists?(fullpath) + end + test "reject regex shortcode", %{message: message} do refute "firedfox" in installed() diff --git a/test/pleroma/web/activity_pub/mrf/tag_policy_test.exs b/test/pleroma/web/activity_pub/mrf/tag_policy_test.exs index 46be316ee..a0db8df54 100644 --- a/test/pleroma/web/activity_pub/mrf/tag_policy_test.exs +++ b/test/pleroma/web/activity_pub/mrf/tag_policy_test.exs @@ -99,6 +99,24 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicyTest do assert TagPolicy.filter(message) == {:ok, except_message} end + + test "removes attachments in Updates" do + actor = insert(:user, tags: ["mrf_tag:media-strip"]) + + message = %{ + "actor" => actor.ap_id, + "type" => "Update", + "object" => %{"attachment" => ["file1"]} + } + + except_message = %{ + "actor" => actor.ap_id, + "type" => "Update", + "object" => %{} + } + + assert TagPolicy.filter(message) == {:ok, except_message} + end end describe "mrf_tag:media-force-nsfw" do @@ -119,5 +137,23 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicyTest do assert TagPolicy.filter(message) == {:ok, except_message} end + + test "Mark as sensitive on presence of attachments in Updates" do + actor = insert(:user, tags: ["mrf_tag:media-force-nsfw"]) + + message = %{ + "actor" => actor.ap_id, + "type" => "Update", + "object" => %{"tag" => ["test"], "attachment" => ["file1"]} + } + + except_message = %{ + "actor" => actor.ap_id, + "type" => "Update", + "object" => %{"tag" => ["test"], "attachment" => ["file1"], "sensitive" => true} + } + + assert TagPolicy.filter(message) == {:ok, except_message} + end end end diff --git a/test/pleroma/web/activity_pub/mrf/utils_test.exs b/test/pleroma/web/activity_pub/mrf/utils_test.exs new file mode 100644 index 000000000..3bbc2cfd3 --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/utils_test.exs @@ -0,0 +1,19 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.UtilsTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Web.ActivityPub.MRF.Utils + + describe "describe_regex_or_string/1" do + test "describes regex" do + assert "~r/foo/i" == Utils.describe_regex_or_string(~r/foo/i) + end + + test "returns string as-is" do + assert "foo" == Utils.describe_regex_or_string("foo") + end + end +end diff --git a/test/pleroma/web/activity_pub/mrf_test.exs b/test/pleroma/web/activity_pub/mrf_test.exs index 8d14e976a..3ead73792 100644 --- a/test/pleroma/web/activity_pub/mrf_test.exs +++ b/test/pleroma/web/activity_pub/mrf_test.exs @@ -1,10 +1,13 @@ # Pleroma: A lightweight social networking server -# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRFTest do - use ExUnit.Case, async: true + use ExUnit.Case use Pleroma.Tests.Helpers + + import ExUnit.CaptureLog + alias Pleroma.Web.ActivityPub.MRF test "subdomains_regex/1" do @@ -61,6 +64,14 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do refute MRF.subdomain_match?(regexes, "EXAMPLE.COM") refute MRF.subdomain_match?(regexes, "example.com") end + + @tag capture_log: true + test "logs sensible error on accidental wildcard" do + assert_raise Regex.CompileError, fn -> + assert capture_log(MRF.subdomains_regex(["*unsafe.tld"])) =~ + "MRF: Invalid subdomain Regex: *unsafe.tld" + end + end end describe "instance_list_from_tuples/1" do diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs index f93537ed8..2b33950d6 100644 --- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest do use Pleroma.DataCase, async: true + alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator alias Pleroma.Web.ActivityPub.Utils @@ -31,6 +32,54 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest test "a basic note validates", %{note: note} do %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) end + + test "a note from factory validates" do + note = insert(:note) + %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note.data) + end + end + + describe "Note with history" do + setup do + user = insert(:user) + {:ok, activity} = Pleroma.Web.CommonAPI.post(user, %{status: "mew mew :dinosaur:"}) + {:ok, edit} = Pleroma.Web.CommonAPI.update(user, activity, %{status: "edited :blank:"}) + + {:ok, %{"object" => external_rep}} = + Pleroma.Web.ActivityPub.Transmogrifier.prepare_outgoing(edit.data) + + %{external_rep: external_rep} + end + + test "edited note", %{external_rep: external_rep} do + assert %{"formerRepresentations" => %{"orderedItems" => [%{"tag" => [_]}]}} = external_rep + + {:ok, validate_res, []} = ObjectValidator.validate(external_rep, []) + + assert %{"formerRepresentations" => %{"orderedItems" => [%{"emoji" => %{"dinosaur" => _}}]}} = + validate_res + end + + test "edited note, badly-formed formerRepresentations", %{external_rep: external_rep} do + external_rep = Map.put(external_rep, "formerRepresentations", %{}) + + assert {:error, _} = ObjectValidator.validate(external_rep, []) + end + + test "edited note, badly-formed history item", %{external_rep: external_rep} do + history_item = + Enum.at(external_rep["formerRepresentations"]["orderedItems"], 0) + |> Map.put("type", "Foo") + + external_rep = + put_in( + external_rep, + ["formerRepresentations", "orderedItems"], + [history_item] + ) + + assert {:error, _} = ObjectValidator.validate(external_rep, []) + end end test "a Note from Roadhouse validates" do @@ -43,4 +92,88 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) end + + test "a Note from Convergence AP Bridge validates" do + insert(:user, ap_id: "https://cc.mkdir.uk/ap/acct/hiira") + + note = + "test/fixtures/ccworld-ap-bridge_note.json" + |> File.read!() + |> Jason.decode!() + + %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) + end + + test "a note with an attachment should work", _ do + insert(:user, %{ap_id: "https://owncast.localhost.localdomain/federation/user/streamer"}) + + note = + "test/fixtures/owncast-note-with-attachment.json" + |> File.read!() + |> Jason.decode!() + + %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) + end + + test "a Note without replies/first/items validates" do + insert(:user, ap_id: "https://mastodon.social/users/emelie") + + note = + "test/fixtures/tesla_mock/status.emelie.json" + |> File.read!() + |> Jason.decode!() + |> pop_in(["replies", "first", "items"]) + |> elem(1) + + %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) + end + + test "Fedibird quote post" do + insert(:user, ap_id: "https://fedibird.com/users/noellabo") + + data = File.read!("test/fixtures/quote_post/fedibird_quote_post.json") |> Jason.decode!() + cng = ArticleNotePageValidator.cast_and_validate(data) + + assert cng.valid? + assert cng.changes.quoteUrl == "https://misskey.io/notes/8vsn2izjwh" + end + + test "Fedibird quote post with quoteUri field" do + insert(:user, ap_id: "https://fedibird.com/users/noellabo") + + data = File.read!("test/fixtures/quote_post/fedibird_quote_uri.json") |> Jason.decode!() + cng = ArticleNotePageValidator.cast_and_validate(data) + + assert cng.valid? + assert cng.changes.quoteUrl == "https://fedibird.com/users/yamako/statuses/107699333438289729" + end + + test "Misskey quote post" do + insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i") + + data = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!() + cng = ArticleNotePageValidator.cast_and_validate(data) + + assert cng.valid? + assert cng.changes.quoteUrl == "https://misskey.io/notes/8vs6wxufd0" + end + + test "Parse tag as quote" do + # https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md + + insert(:user, ap_id: "https://server.example/users/1") + + data = File.read!("test/fixtures/quote_post/fep-e232-tag-example.json") |> Jason.decode!() + cng = ArticleNotePageValidator.cast_and_validate(data) + + assert cng.valid? + assert cng.changes.quoteUrl == "https://server.example/objects/123" + + assert Enum.at(cng.changes.tag, 0).changes == %{ + type: "Link", + mediaType: "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", + href: "https://server.example/objects/123", + name: "RE: https://server.example/objects/123" + } + end end diff --git a/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs index 9a17e277e..a615c1d9a 100644 --- a/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs @@ -5,12 +5,27 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidatorTest do use Pleroma.DataCase, async: true + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator + import Mox import Pleroma.Factory describe "attachments" do + test "fails without url" do + attachment = %{ + "mediaType" => "", + "name" => "", + "summary" => "298p3RG7j27tfsZ9RQ.jpg", + "type" => "Document" + } + + assert {:error, _cng} = + AttachmentValidator.cast_and_validate(attachment) + |> Ecto.Changeset.apply_action(:insert) + end + test "works with honkerific attachments" do attachment = %{ "mediaType" => "", @@ -103,6 +118,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidatorTest do filename: "an_image.jpg" } + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) {:ok, attachment} = @@ -146,7 +164,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidatorTest do assert attachment.mediaType == "image/jpeg" end - test "it transforms image dimentions to our internal format" do + test "it transforms image dimensions to our internal format" do attachment = %{ "type" => "Document", "name" => "Hello world", diff --git a/test/pleroma/web/activity_pub/object_validators/chat_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/chat_validation_test.exs index 8192efe97..301fed60d 100644 --- a/test/pleroma/web/activity_pub/object_validators/chat_validation_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/chat_validation_test.exs @@ -5,11 +5,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatValidationTest do use Pleroma.DataCase alias Pleroma.Object + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.CommonAPI + import Mox import Pleroma.Factory describe "chat message create activities" do @@ -82,6 +84,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatValidationTest do filename: "an_image.jpg" } + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) valid_chat_message = @@ -103,6 +108,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatValidationTest do filename: "an_image.jpg" } + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) valid_chat_message = @@ -124,6 +132,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatValidationTest do filename: "an_image.jpg" } + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + {:ok, attachment} = ActivityPub.upload(file, actor: user.ap_id) valid_chat_message = @@ -136,6 +147,21 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatValidationTest do assert object["attachment"] end + test "validates for a basic object with content but attachment set to empty array", %{ + user: user, + recipient: recipient + } do + {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "Hello!") + + valid_chat_message = + valid_chat_message + |> Map.put("attachment", []) + + assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, []) + + assert object == Map.drop(valid_chat_message, ["attachment"]) + end + test "does not validate if the message has no content", %{ valid_chat_message: valid_chat_message } do diff --git a/test/pleroma/web/activity_pub/object_validators/create_generic_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/create_generic_validator_test.exs index 0a5b44beb..e771260c9 100644 --- a/test/pleroma/web/activity_pub/object_validators/create_generic_validator_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/create_generic_validator_test.exs @@ -23,10 +23,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidatorTest do {:ok, object_data} = ObjectValidator.cast_and_apply(note_activity["object"]) meta = [object_data: ObjectValidator.stringify_keys(object_data)] - %{valid?: true} = CreateGenericValidator.cast_and_validate(note_activity, meta) + assert %{valid?: true} = CreateGenericValidator.cast_and_validate(note_activity, meta) end - test "a Create/Note with mismatched context is invalid" do + test "a Create/Note with mismatched context uses the Note's context" do user = insert(:user) note = %{ @@ -54,6 +54,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidatorTest do {:ok, object_data} = ObjectValidator.cast_and_apply(note_activity["object"]) meta = [object_data: ObjectValidator.stringify_keys(object_data)] - %{valid?: false} = CreateGenericValidator.cast_and_validate(note_activity, meta) + validated = CreateGenericValidator.cast_and_validate(note_activity, meta) + + assert validated.valid? + assert {:context, note["context"]} in validated.changes end end diff --git a/test/pleroma/web/activity_pub/object_validators/delete_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/delete_validation_test.exs index ea4664859..bbb31516c 100644 --- a/test/pleroma/web/activity_pub/object_validators/delete_validation_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/delete_validation_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidationTest do - use Pleroma.DataCase, async: true + use Pleroma.DataCase, async: false alias Pleroma.Object alias Pleroma.Web.ActivityPub.Builder @@ -90,17 +90,26 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidationTest do assert {:actor, {"is not allowed to modify object", []}} in cng.errors end - test "it's valid if the actor of the object is a local superuser", + test "it's only valid if the actor of the object is a privileged local user", %{valid_post_delete: valid_post_delete} do + clear_config([:instance, :moderator_privileges], [:messages_delete]) + user = insert(:user, local: true, is_moderator: true, ap_id: "https://gensokyo.2hu/users/raymoo") - valid_other_actor = + post_delete_with_moderator_actor = valid_post_delete |> Map.put("actor", user.ap_id) - {:ok, _, meta} = ObjectValidator.validate(valid_other_actor, []) + {:ok, _, meta} = ObjectValidator.validate(post_delete_with_moderator_actor, []) + assert meta[:do_not_federate] + + clear_config([:instance, :moderator_privileges], []) + + {:error, cng} = ObjectValidator.validate(post_delete_with_moderator_actor, []) + + assert {:actor, {"is not allowed to modify object", []}} in cng.errors end end end diff --git a/test/pleroma/web/activity_pub/object_validators/emoji_react_handling_test.exs b/test/pleroma/web/activity_pub/object_validators/emoji_react_handling_test.exs index bbdb09c4c..9bb291a38 100644 --- a/test/pleroma/web/activity_pub/object_validators/emoji_react_handling_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/emoji_react_handling_test.exs @@ -38,16 +38,70 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactHandlingTest do assert {:content, {"can't be blank", [validation: :required]}} in cng.errors end - test "it is not valid with a non-emoji content field", %{valid_emoji_react: valid_emoji_react} do + test "it is valid when custom emoji is used", %{valid_emoji_react: valid_emoji_react} do without_emoji_content = valid_emoji_react - |> Map.put("content", "x") + |> Map.put("content", ":hello:") + |> Map.put("tag", [ + %{ + "type" => "Emoji", + "name" => ":hello:", + "icon" => %{"url" => "http://somewhere", "type" => "Image"} + } + ]) + + {:ok, _, _} = ObjectValidator.validate(without_emoji_content, []) + end + + test "it is not valid when custom emoji don't have a matching tag", %{ + valid_emoji_react: valid_emoji_react + } do + without_emoji_content = + valid_emoji_react + |> Map.put("content", ":hello:") + |> Map.put("tag", [ + %{ + "type" => "Emoji", + "name" => ":whoops:", + "icon" => %{"url" => "http://somewhere", "type" => "Image"} + } + ]) + + {:error, cng} = ObjectValidator.validate(without_emoji_content, []) + + refute cng.valid? + + assert {:tag, {"does not contain an Emoji tag", []}} in cng.errors + end + + test "it is not valid when custom emoji have no tags", %{ + valid_emoji_react: valid_emoji_react + } do + without_emoji_content = + valid_emoji_react + |> Map.put("content", ":hello:") + |> Map.put("tag", []) + + {:error, cng} = ObjectValidator.validate(without_emoji_content, []) + + refute cng.valid? + + assert {:tag, {"does not contain an Emoji tag", []}} in cng.errors + end + + test "it is not valid when custom emoji doesn't match a shortcode format", %{ + valid_emoji_react: valid_emoji_react + } do + without_emoji_content = + valid_emoji_react + |> Map.put("content", "hello") + |> Map.put("tag", []) {:error, cng} = ObjectValidator.validate(without_emoji_content, []) refute cng.valid? - assert {:content, {"must be a single character emoji", []}} in cng.errors + assert {:tag, {"does not contain an Emoji tag", []}} in cng.errors end end end diff --git a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs index 94bc5a89b..a09dbf5c6 100644 --- a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs @@ -32,7 +32,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do test "returns an error if the object can't be updated by the actor", %{ valid_update: valid_update } do - other_user = insert(:user) + other_user = insert(:user, local: false) update = valid_update @@ -40,5 +40,129 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do assert {:error, _cng} = ObjectValidator.validate(update, []) end + + test "validates as long as the object is same-origin with the actor", %{ + valid_update: valid_update + } do + other_user = insert(:user) + + update = + valid_update + |> Map.put("actor", other_user.ap_id) + + assert {:ok, _update, []} = ObjectValidator.validate(update, []) + end + + test "validates if the object is not of an Actor type" do + note = insert(:note) + updated_note = note.data |> Map.put("content", "edited content") + other_user = insert(:user) + + {:ok, update, _} = Builder.update(other_user, updated_note) + + assert {:ok, _update, _} = ObjectValidator.validate(update, []) + end + end + + describe "update note" do + test "converts object into Pleroma's format" do + mastodon_tags = [ + %{ + "icon" => %{ + "mediaType" => "image/png", + "type" => "Image", + "url" => "https://somewhere.org/emoji/url/1.png" + }, + "id" => "https://somewhere.org/emoji/1", + "name" => ":some_emoji:", + "type" => "Emoji", + "updated" => "2021-04-07T11:00:00Z" + } + ] + + user = insert(:user) + note = insert(:note, user: user) + + updated_note = + note.data + |> Map.put("content", "edited content") + |> Map.put("tag", mastodon_tags) + + {:ok, update, _} = Builder.update(user, updated_note) + + assert {:ok, _update, meta} = ObjectValidator.validate(update, []) + + assert %{"emoji" => %{"some_emoji" => "https://somewhere.org/emoji/url/1.png"}} = + meta[:object_data] + end + + test "returns no object_data in meta for a local Update" do + user = insert(:user) + note = insert(:note, user: user) + + updated_note = + note.data + |> Map.put("content", "edited content") + + {:ok, update, _} = Builder.update(user, updated_note) + + assert {:ok, _update, meta} = ObjectValidator.validate(update, local: true) + assert is_nil(meta[:object_data]) + end + + test "returns object_data in meta for a remote Update" do + user = insert(:user) + note = insert(:note, user: user) + + updated_note = + note.data + |> Map.put("content", "edited content") + + {:ok, update, _} = Builder.update(user, updated_note) + + assert {:ok, _update, meta} = ObjectValidator.validate(update, local: false) + assert meta[:object_data] + + assert {:ok, _update, meta} = ObjectValidator.validate(update, []) + assert meta[:object_data] + end + end + + describe "update with history" do + setup do + user = insert(:user) + {:ok, activity} = Pleroma.Web.CommonAPI.post(user, %{status: "mew mew :dinosaur:"}) + {:ok, edit} = Pleroma.Web.CommonAPI.update(user, activity, %{status: "edited :blank:"}) + {:ok, external_rep} = Pleroma.Web.ActivityPub.Transmogrifier.prepare_outgoing(edit.data) + %{external_rep: external_rep} + end + + test "edited note", %{external_rep: external_rep} do + {:ok, _validate_res, meta} = ObjectValidator.validate(external_rep, []) + + assert %{"formerRepresentations" => %{"orderedItems" => [%{"emoji" => %{"dinosaur" => _}}]}} = + meta[:object_data] + end + + test "edited note, badly-formed formerRepresentations", %{external_rep: external_rep} do + external_rep = put_in(external_rep, ["object", "formerRepresentations"], %{}) + + assert {:error, _} = ObjectValidator.validate(external_rep, []) + end + + test "edited note, badly-formed history item", %{external_rep: external_rep} do + history_item = + Enum.at(external_rep["object"]["formerRepresentations"]["orderedItems"], 0) + |> Map.put("type", "Foo") + + external_rep = + put_in( + external_rep, + ["object", "formerRepresentations", "orderedItems"], + [history_item] + ) + + assert {:error, _} = ObjectValidator.validate(external_rep, []) + end end end diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs index e2db3d575..870f1f77a 100644 --- a/test/pleroma/web/activity_pub/publisher_test.exs +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -25,6 +25,17 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do setup_all do: clear_config([:instance, :federating], true) + describe "should_federate?/1" do + test "it returns false when the inbox is nil" do + refute Publisher.should_federate?(nil, false) + refute Publisher.should_federate?(nil, true) + end + + test "it returns true when public is true" do + assert Publisher.should_federate?(false, true) + end + end + describe "gather_webfinger_links/1" do test "it returns links" do user = insert(:user) @@ -205,6 +216,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do refute called(Instances.set_reachable(inbox)) end + @tag capture_log: true test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code", Instances, [:passthrough], @@ -212,7 +224,8 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do actor = insert(:user) inbox = "http://404.site/users/nick1/inbox" - assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + assert {:discard, _} = + Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) assert called(Instances.set_unreachable(inbox)) end @@ -268,7 +281,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do describe "publish/2" do test_with_mock "doesn't publish a non-public activity to quarantined instances.", - Pleroma.Web.Federator.Publisher, + Pleroma.Web.ActivityPub.Publisher, [:passthrough], [] do Config.put([:instance, :quarantined_instances], [{"domain.com", "some reason"}]) @@ -276,8 +289,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do follower = insert(:user, %{ local: false, - inbox: "https://domain.com/users/nick1/inbox", - ap_enabled: true + inbox: "https://domain.com/users/nick1/inbox" }) actor = insert(:user, follower_address: follower.ap_id) @@ -296,7 +308,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do assert res == :ok assert not called( - Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{ + Publisher.enqueue_one(%{ inbox: "https://domain.com/users/nick1/inbox", actor_id: actor.id, id: note_activity.data["id"] @@ -305,7 +317,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do end test_with_mock "Publishes a non-public activity to non-quarantined instances.", - Pleroma.Web.Federator.Publisher, + Pleroma.Web.ActivityPub.Publisher, [:passthrough], [] do Config.put([:instance, :quarantined_instances], [{"somedomain.com", "some reason"}]) @@ -313,8 +325,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do follower = insert(:user, %{ local: false, - inbox: "https://domain.com/users/nick1/inbox", - ap_enabled: true + inbox: "https://domain.com/users/nick1/inbox" }) actor = insert(:user, follower_address: follower.ap_id) @@ -333,23 +344,49 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do assert res == :ok assert called( - Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{ - inbox: "https://domain.com/users/nick1/inbox", - actor_id: actor.id, - id: note_activity.data["id"] - }) + Publisher.enqueue_one( + %{ + inbox: "https://domain.com/users/nick1/inbox", + actor_id: actor.id, + id: note_activity.data["id"] + }, + priority: 1 + ) + ) + end + + test_with_mock "Publishes to directly addressed actors with higher priority.", + Pleroma.Web.ActivityPub.Publisher, + [:passthrough], + [] do + note_activity = insert(:direct_note_activity) + + actor = Pleroma.User.get_by_ap_id(note_activity.data["actor"]) + + res = Publisher.publish(actor, note_activity) + + assert res == :ok + + assert called( + Publisher.enqueue_one( + %{ + inbox: :_, + actor_id: actor.id, + id: note_activity.data["id"] + }, + priority: 0 + ) ) end test_with_mock "publishes an activity with BCC to all relevant peers.", - Pleroma.Web.Federator.Publisher, + Pleroma.Web.ActivityPub.Publisher, [:passthrough], [] do follower = insert(:user, %{ local: false, - inbox: "https://domain.com/users/nick1/inbox", - ap_enabled: true + inbox: "https://domain.com/users/nick1/inbox" }) actor = insert(:user, follower_address: follower.ap_id) @@ -367,7 +404,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do assert res == :ok assert called( - Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{ + Publisher.enqueue_one(%{ inbox: "https://domain.com/users/nick1/inbox", actor_id: actor.id, id: note_activity.data["id"] @@ -376,21 +413,19 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do end test_with_mock "publishes a delete activity to peers who signed fetch requests to the create acitvity/object.", - Pleroma.Web.Federator.Publisher, + Pleroma.Web.ActivityPub.Publisher, [:passthrough], [] do fetcher = insert(:user, local: false, - inbox: "https://domain.com/users/nick1/inbox", - ap_enabled: true + inbox: "https://domain.com/users/nick1/inbox" ) another_fetcher = insert(:user, local: false, - inbox: "https://domain2.com/users/nick1/inbox", - ap_enabled: true + inbox: "https://domain2.com/users/nick1/inbox" ) actor = insert(:user) @@ -419,19 +454,25 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do assert res == :ok assert called( - Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{ - inbox: "https://domain.com/users/nick1/inbox", - actor_id: actor.id, - id: delete.data["id"] - }) + Publisher.enqueue_one( + %{ + inbox: "https://domain.com/users/nick1/inbox", + actor_id: actor.id, + id: delete.data["id"] + }, + priority: 1 + ) ) assert called( - Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{ - inbox: "https://domain2.com/users/nick1/inbox", - actor_id: actor.id, - id: delete.data["id"] - }) + Publisher.enqueue_one( + %{ + inbox: "https://domain2.com/users/nick1/inbox", + actor_id: actor.id, + id: delete.data["id"] + }, + priority: 1 + ) ) end end diff --git a/test/pleroma/web/activity_pub/side_effects_test.exs b/test/pleroma/web/activity_pub/side_effects_test.exs index 64c4a8c14..7af50e12c 100644 --- a/test/pleroma/web/activity_pub/side_effects_test.exs +++ b/test/pleroma/web/activity_pub/side_effects_test.exs @@ -17,11 +17,19 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.SideEffects + alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.ActivityDraft import Mock import Pleroma.Factory + defp get_announces_of_object(%{data: %{"id" => id}} = _object) do + Pleroma.Activity.Queries.by_type("Announce") + |> Pleroma.Activity.Queries.by_object_id(id) + |> Pleroma.Repo.all() + end + describe "handle_after_transaction" do test "it streams out notifications and streams" do author = insert(:user, local: true) @@ -118,7 +126,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do describe "update users" do setup do user = insert(:user, local: false) - {:ok, update_data, []} = Builder.update(user, %{"id" => user.ap_id, "name" => "new name!"}) + + {:ok, update_data, []} = + Builder.update(user, %{"id" => user.ap_id, "type" => "Person", "name" => "new name!"}) + {:ok, update, _meta} = ActivityPub.persist(update_data, local: true) %{user: user, update_data: update_data, update: update} @@ -140,6 +151,298 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do end end + describe "update notes" do + setup do + make_time = fn -> + Pleroma.Web.ActivityPub.Utils.make_date() + end + + user = insert(:user) + note = insert(:note, user: user, data: %{"published" => make_time.()}) + _note_activity = insert(:note_activity, note: note) + + updated_note = + note.data + |> Map.put("summary", "edited summary") + |> Map.put("content", "edited content") + |> Map.put("updated", make_time.()) + + {:ok, update_data, []} = Builder.update(user, updated_note) + {:ok, update, _meta} = ActivityPub.persist(update_data, local: true) + + %{ + user: user, + note: note, + object_id: note.id, + update_data: update_data, + update: update, + updated_note: updated_note + } + end + + test "it updates the note", %{ + object_id: object_id, + update: update, + updated_note: updated_note + } do + {:ok, _, _} = SideEffects.handle(update, object_data: updated_note) + updated_time = updated_note["updated"] + + new_note = Pleroma.Object.get_by_id(object_id) + + assert %{ + "summary" => "edited summary", + "content" => "edited content", + "updated" => ^updated_time + } = new_note.data + end + + test "it rejects updates with no updated attribute in object", %{ + object_id: object_id, + update: update, + updated_note: updated_note + } do + old_note = Pleroma.Object.get_by_id(object_id) + updated_note = Map.drop(updated_note, ["updated"]) + {:ok, _, _} = SideEffects.handle(update, object_data: updated_note) + new_note = Pleroma.Object.get_by_id(object_id) + assert old_note.data == new_note.data + end + + test "it rejects updates with updated attribute older than what we have in the original object", + %{ + object_id: object_id, + update: update, + updated_note: updated_note + } do + old_note = Pleroma.Object.get_by_id(object_id) + {:ok, creation_time, _} = DateTime.from_iso8601(old_note.data["published"]) + + updated_note = + Map.put(updated_note, "updated", DateTime.to_iso8601(DateTime.add(creation_time, -10))) + + {:ok, _, _} = SideEffects.handle(update, object_data: updated_note) + new_note = Pleroma.Object.get_by_id(object_id) + assert old_note.data == new_note.data + end + + test "it rejects updates with updated attribute older than the last Update", %{ + object_id: object_id, + update: update, + updated_note: updated_note + } do + old_note = Pleroma.Object.get_by_id(object_id) + {:ok, creation_time, _} = DateTime.from_iso8601(old_note.data["published"]) + + updated_note = + Map.put(updated_note, "updated", DateTime.to_iso8601(DateTime.add(creation_time, +10))) + + {:ok, _, _} = SideEffects.handle(update, object_data: updated_note) + + old_note = Pleroma.Object.get_by_id(object_id) + {:ok, update_time, _} = DateTime.from_iso8601(old_note.data["updated"]) + + updated_note = + Map.put(updated_note, "updated", DateTime.to_iso8601(DateTime.add(update_time, -5))) + + {:ok, _, _} = SideEffects.handle(update, object_data: updated_note) + + new_note = Pleroma.Object.get_by_id(object_id) + assert old_note.data == new_note.data + end + + test "it updates using object_data", %{ + object_id: object_id, + update: update, + updated_note: updated_note + } do + updated_note = Map.put(updated_note, "summary", "mew mew") + {:ok, _, _} = SideEffects.handle(update, object_data: updated_note) + new_note = Pleroma.Object.get_by_id(object_id) + assert %{"summary" => "mew mew", "content" => "edited content"} = new_note.data + end + + test "it records the original note in formerRepresentations", %{ + note: note, + object_id: object_id, + update: update, + updated_note: updated_note + } do + {:ok, _, _} = SideEffects.handle(update, object_data: updated_note) + %{data: new_note} = Pleroma.Object.get_by_id(object_id) + assert %{"summary" => "edited summary", "content" => "edited content"} = new_note + + assert [Map.drop(note.data, ["id", "formerRepresentations"])] == + new_note["formerRepresentations"]["orderedItems"] + + assert new_note["formerRepresentations"]["totalItems"] == 1 + end + + test "it puts the original note at the front of formerRepresentations", %{ + user: user, + note: note, + object_id: object_id, + update: update, + updated_note: updated_note + } do + {:ok, _, _} = SideEffects.handle(update, object_data: updated_note) + %{data: first_edit} = Pleroma.Object.get_by_id(object_id) + + second_updated_note = + note.data + |> Map.put("summary", "edited summary 2") + |> Map.put("content", "edited content 2") + |> Map.put( + "updated", + first_edit["updated"] + |> DateTime.from_iso8601() + |> elem(1) + |> DateTime.add(10) + |> DateTime.to_iso8601() + ) + + {:ok, second_update_data, []} = Builder.update(user, second_updated_note) + {:ok, update, _meta} = ActivityPub.persist(second_update_data, local: true) + {:ok, _, _} = SideEffects.handle(update, object_data: second_updated_note) + %{data: new_note} = Pleroma.Object.get_by_id(object_id) + assert %{"summary" => "edited summary 2", "content" => "edited content 2"} = new_note + + original_version = Map.drop(note.data, ["id", "formerRepresentations"]) + first_edit = Map.drop(first_edit, ["id", "formerRepresentations"]) + + assert [first_edit, original_version] == + new_note["formerRepresentations"]["orderedItems"] + + assert new_note["formerRepresentations"]["totalItems"] == 2 + end + + test "it does not prepend to formerRepresentations if no actual changes are made", %{ + note: note, + object_id: object_id, + update: update, + updated_note: updated_note + } do + {:ok, _, _} = SideEffects.handle(update, object_data: updated_note) + %{data: first_edit} = Pleroma.Object.get_by_id(object_id) + + updated_note = + updated_note + |> Map.put( + "updated", + first_edit["updated"] + |> DateTime.from_iso8601() + |> elem(1) + |> DateTime.add(10) + |> DateTime.to_iso8601() + ) + + {:ok, _, _} = SideEffects.handle(update, object_data: updated_note) + %{data: new_note} = Pleroma.Object.get_by_id(object_id) + assert %{"summary" => "edited summary", "content" => "edited content"} = new_note + + original_version = Map.drop(note.data, ["id", "formerRepresentations"]) + + assert [original_version] == + new_note["formerRepresentations"]["orderedItems"] + + assert new_note["formerRepresentations"]["totalItems"] == 1 + end + end + + describe "update questions" do + setup do + user = insert(:user) + + question = + insert(:question, + user: user, + data: %{"published" => Pleroma.Web.ActivityPub.Utils.make_date()} + ) + + %{user: user, data: question.data, id: question.id} + end + + test "allows updating choice count without generating edit history", %{ + user: user, + data: data, + id: id + } do + new_choices = + data["oneOf"] + |> Enum.map(fn choice -> put_in(choice, ["replies", "totalItems"], 5) end) + + updated_question = + data + |> Map.put("oneOf", new_choices) + |> Map.put("updated", Pleroma.Web.ActivityPub.Utils.make_date()) + + {:ok, update_data, []} = Builder.update(user, updated_question) + {:ok, update, _meta} = ActivityPub.persist(update_data, local: true) + + {:ok, _, _} = SideEffects.handle(update, object_data: updated_question) + + %{data: new_question} = Pleroma.Object.get_by_id(id) + + assert [%{"replies" => %{"totalItems" => 5}}, %{"replies" => %{"totalItems" => 5}}] = + new_question["oneOf"] + + refute Map.has_key?(new_question, "formerRepresentations") + end + + test "allows updating choice count without updated field", %{ + user: user, + data: data, + id: id + } do + new_choices = + data["oneOf"] + |> Enum.map(fn choice -> put_in(choice, ["replies", "totalItems"], 5) end) + + updated_question = + data + |> Map.put("oneOf", new_choices) + + {:ok, update_data, []} = Builder.update(user, updated_question) + {:ok, update, _meta} = ActivityPub.persist(update_data, local: true) + + {:ok, _, _} = SideEffects.handle(update, object_data: updated_question) + + %{data: new_question} = Pleroma.Object.get_by_id(id) + + assert [%{"replies" => %{"totalItems" => 5}}, %{"replies" => %{"totalItems" => 5}}] = + new_question["oneOf"] + + refute Map.has_key?(new_question, "formerRepresentations") + end + + test "allows updating choice count with updated field same as the creation date", %{ + user: user, + data: data, + id: id + } do + new_choices = + data["oneOf"] + |> Enum.map(fn choice -> put_in(choice, ["replies", "totalItems"], 5) end) + + updated_question = + data + |> Map.put("oneOf", new_choices) + |> Map.put("updated", data["published"]) + + {:ok, update_data, []} = Builder.update(user, updated_question) + {:ok, update, _meta} = ActivityPub.persist(update_data, local: true) + + {:ok, _, _} = SideEffects.handle(update, object_data: updated_question) + + %{data: new_question} = Pleroma.Object.get_by_id(id) + + assert [%{"replies" => %{"totalItems" => 5}}, %{"replies" => %{"totalItems" => 5}}] = + new_question["oneOf"] + + refute Map.has_key?(new_question, "formerRepresentations") + end + end + describe "EmojiReact objects" do setup do poster = insert(:user) @@ -158,7 +461,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do object = Object.get_by_ap_id(emoji_react.data["object"]) assert object.data["reaction_count"] == 1 - assert ["👌", [user.ap_id]] in object.data["reactions"] + assert ["👌", [user.ap_id], nil] in object.data["reactions"] end test "creates a notification", %{emoji_react: emoji_react, poster: poster} do @@ -524,33 +827,6 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do {:ok, announce, _} = SideEffects.handle(announce) assert Repo.get_by(Notification, user_id: poster.id, activity_id: announce.id) end - - test "it streams out the announce", %{announce: announce} do - with_mocks([ - { - Pleroma.Web.Streamer, - [], - [ - stream: fn _, _ -> nil end - ] - }, - { - Pleroma.Web.Push, - [], - [ - send: fn _ -> nil end - ] - } - ]) do - {:ok, announce, _} = SideEffects.handle(announce) - - assert called( - Pleroma.Web.Streamer.stream(["user", "list", "public", "public:local"], announce) - ) - - assert called(Pleroma.Web.Push.send(:_)) - end - end end describe "removing a follower" do @@ -622,4 +898,85 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do assert User.get_follow_state(user, followed, nil) == nil end end + + describe "Group actors" do + setup do + poster = + insert(:user, + local: false, + nickname: "poster@example.com", + ap_id: "https://example.com/users/poster" + ) + + group = insert(:user, actor_type: "Group") + + make_create = fn mentioned_users -> + mentions = mentioned_users |> Enum.map(fn u -> "@#{u.nickname}" end) |> Enum.join(" ") + {:ok, draft} = ActivityDraft.create(poster, %{status: "#{mentions} hey"}) + + create_activity_data = + Utils.make_create_data(draft.changes |> Map.put(:published, nil), %{}) + |> put_in(["object", "id"], "https://example.com/object") + |> put_in(["id"], "https://example.com/activity") + + assert Enum.all?(mentioned_users, fn u -> u.ap_id in create_activity_data["to"] end) + + create_activity_data + end + + %{poster: poster, group: group, make_create: make_create} + end + + test "group should boost it", %{make_create: make_create, group: group} do + create_activity_data = make_create.([group]) + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, _meta} = + SideEffects.handle(create_activity, + local: false, + object_data: create_activity_data["object"] + ) + + object = Object.normalize(create_activity, fetch: false) + assert [announce] = get_announces_of_object(object) + assert announce.actor == group.ap_id + end + + test "remote group should not boost it", %{make_create: make_create, group: group} do + remote_group = + insert(:user, actor_type: "Group", local: false, nickname: "remotegroup@example.com") + + create_activity_data = make_create.([group, remote_group]) + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, _meta} = + SideEffects.handle(create_activity, + local: false, + object_data: create_activity_data["object"] + ) + + object = Object.normalize(create_activity, fetch: false) + assert [announce] = get_announces_of_object(object) + assert announce.actor == group.ap_id + end + + test "group should not boost it if group is blocking poster", %{ + make_create: make_create, + group: group, + poster: poster + } do + {:ok, _} = CommonAPI.block(group, poster) + create_activity_data = make_create.([group]) + {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false) + + {:ok, _create_activity, _meta} = + SideEffects.handle(create_activity, + local: false, + object_data: create_activity_data["object"] + ) + + object = Object.normalize(create_activity, fetch: false) + assert [] = get_announces_of_object(object) + end + end end diff --git a/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs index ceedb185f..f2e1cefa3 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs @@ -34,7 +34,87 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do object = Object.get_by_ap_id(data["object"]) assert object.data["reaction_count"] == 1 - assert match?([["👌", _]], object.data["reactions"]) + assert match?([["👌", _, nil]], object.data["reactions"]) + end + + test "it works for incoming custom emoji reactions" do + user = insert(:user) + other_user = insert(:user, local: false) + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/custom-emoji-reaction.json") + |> Jason.decode!() + |> Map.put("object", activity.data["object"]) + |> Map.put("actor", other_user.ap_id) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == other_user.ap_id + assert data["type"] == "EmojiReact" + assert data["id"] == "https://misskey.local.live/likes/917ocsybgp" + assert data["object"] == activity.data["object"] + assert data["content"] == ":hanapog:" + + assert data["tag"] == [ + %{ + "id" => "https://misskey.local.live/emojis/hanapog", + "type" => "Emoji", + "name" => "hanapog", + "updated" => "2022-06-07T12:00:05.773Z", + "icon" => %{ + "type" => "Image", + "url" => + "https://misskey.local.live/files/webpublic-8f8a9768-7264-4171-88d6-2356aabeadcd" + } + } + ] + + object = Object.get_by_ap_id(data["object"]) + + assert object.data["reaction_count"] == 1 + + assert match?( + [ + [ + "hanapog", + _, + "https://misskey.local.live/files/webpublic-8f8a9768-7264-4171-88d6-2356aabeadcd" + ] + ], + object.data["reactions"] + ) + end + + test "it works for incoming unqualified emoji reactions" do + user = insert(:user) + other_user = insert(:user, local: false) + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + # woman detective emoji, unqualified + unqualified_emoji = [0x1F575, 0x200D, 0x2640] |> List.to_string() + + data = + File.read!("test/fixtures/emoji-reaction.json") + |> Jason.decode!() + |> Map.put("object", activity.data["object"]) + |> Map.put("actor", other_user.ap_id) + |> Map.put("content", unqualified_emoji) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == other_user.ap_id + assert data["type"] == "EmojiReact" + assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2" + assert data["object"] == activity.data["object"] + # woman detective emoji, fully qualified + emoji = [0x1F575, 0xFE0F, 0x200D, 0x2640, 0xFE0F] |> List.to_string() + assert data["content"] == emoji + + object = Object.get_by_ap_id(data["object"]) + + assert object.data["reaction_count"] == 1 + assert match?([[^emoji, _, _]], object.data["reactions"]) end test "it reject invalid emoji reactions" do diff --git a/test/pleroma/web/activity_pub/transmogrifier/image_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/image_handling_test.exs new file mode 100644 index 000000000..b85f0a477 --- /dev/null +++ b/test/pleroma/web/activity_pub/transmogrifier/image_handling_test.exs @@ -0,0 +1,50 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Transmogrifier.ImageHandlingTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + alias Pleroma.Activity + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.Transmogrifier + + test "Hubzilla Image object" do + Tesla.Mock.mock(fn + %{url: "https://hub.somaton.com/channel/testc6"} -> + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/hubzilla-actor.json"), + headers: HttpRequestMock.activitypub_object_headers() + } + end) + + data = File.read!("test/fixtures/hubzilla-create-image.json") |> Poison.decode!() + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + assert object = Object.normalize(activity, fetch: false) + + assert object.data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] + + assert object.data["cc"] == ["https://hub.somaton.com/followers/testc6"] + + assert object.data["attachment"] == [ + %{ + "mediaType" => "image/jpeg", + "type" => "Link", + "url" => [ + %{ + "height" => 2200, + "href" => + "https://hub.somaton.com/photo/452583b2-7e1f-4ac3-8334-ff666f134afe-0.jpg", + "mediaType" => "image/jpeg", + "type" => "Link", + "width" => 2200 + } + ] + } + ] + end +end diff --git a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs index b00fd919b..2507fa2b0 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs @@ -104,6 +104,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do end end + @tag capture_log: true test "it does not crash if the object in inReplyTo can't be fetched" do data = File.read!("test/fixtures/mastodon-post-activity.json") @@ -220,6 +221,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do "<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>" end + test "it works for incoming notices with a nil contentMap (firefish)" do + data = + File.read!("test/fixtures/mastodon-post-activity-contentmap.json") + |> Jason.decode!() + |> Map.put("contentMap", nil) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + object = Object.normalize(data["object"], fetch: false) + + assert object.data["content"] == + "<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>" + end + test "it works for incoming notices with to/cc not being an array (kroeg)" do data = File.read!("test/fixtures/kroeg-post-activity.json") |> Jason.decode!() @@ -507,7 +521,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do [data: data] end - test "returns not modified object when hasn't containts inReplyTo field", %{data: data} do + test "returns not modified object when has no inReplyTo field", %{data: data} do assert Transmogrifier.fix_in_reply_to(data) == data end @@ -707,4 +721,43 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do } ] end + + test "the standalone note uses its own ID when context is missing" do + insert(:user, ap_id: "https://mk.absturztau.be/users/8ozbzjs3o8") + + activity = + "test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json" + |> File.read!() + |> Jason.decode!() + + {:ok, %Activity{} = modified} = Transmogrifier.handle_incoming(activity) + object = Object.normalize(modified, fetch: false) + + assert object.data["context"] == object.data["id"] + assert modified.data["context"] == object.data["id"] + end + + @tag capture_log: true + test "the reply note uses its parent's ID when context is missing and reply is unreachable" do + insert(:user, ap_id: "https://mk.absturztau.be/users/8ozbzjs3o8") + + activity = + "test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json" + |> File.read!() + |> Jason.decode!() + + object = + activity["object"] + |> Map.put("inReplyTo", "https://404.site/object/went-to-buy-milk") + + activity = + activity + |> Map.put("object", object) + + {:ok, %Activity{} = modified} = Transmogrifier.handle_incoming(activity) + object = Object.normalize(modified, fetch: false) + + assert object.data["context"] == object.data["inReplyTo"] + assert modified.data["context"] == object.data["inReplyTo"] + end end diff --git a/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs index d22ec400d..d31070546 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs @@ -33,8 +33,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.QuestionHandlingTest do assert object.data["context"] == "tag:mastodon.sdf.org,2019-05-10:objectId=15095122:objectType=Conversation" - assert object.data["context_id"] - assert object.data["anyOf"] == [] assert Enum.sort(object.data["oneOf"]) == @@ -68,7 +66,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.QuestionHandlingTest do reply_object = Object.normalize(reply_activity, fetch: false) assert reply_object.data["context"] == object.data["context"] - assert reply_object.data["context_id"] == object.data["context_id"] end test "Mastodon Question activity with HTML tags in plaintext" do diff --git a/test/pleroma/web/activity_pub/transmogrifier/undo_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/undo_handling_test.exs index 846d25cbe..ea01c92fa 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/undo_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/undo_handling_test.exs @@ -32,7 +32,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.UndoHandlingTest do assert activity.data["type"] == "Undo" end - test "it returns an error for incoming unlikes wihout a like activity" do + test "it returns an error for incoming unlikes without a like activity" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: "leave a like pls"}) diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index 335fe1a30..a49e459a6 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -8,7 +8,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do alias Pleroma.Activity alias Pleroma.Object - alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Utils @@ -61,7 +60,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do note_obj = %{ "type" => "Note", - "id" => activity.data["id"], + "id" => activity.object.data["id"], "content" => "test post", "published" => object.data["published"], "actor" => AccountView.render("show.json", %{user: user, skip_visibility_check: true}) @@ -108,15 +107,54 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert activity.data["type"] == "Move" end - test "a reply with mismatched context is rejected" do - insert(:user, ap_id: "https://macgirvin.com/channel/mike") + test "it fixes both the Create and object contexts in a reply" do + insert(:user, ap_id: "https://mk.absturztau.be/users/8ozbzjs3o8") + insert(:user, ap_id: "https://p.helene.moe/users/helene") - note_activity = - "test/fixtures/roadhouse-create-activity.json" + create_activity = + "test/fixtures/create-pleroma-reply-to-misskey-thread.json" |> File.read!() |> Jason.decode!() - assert {:error, _} = Transmogrifier.handle_incoming(note_activity) + assert {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(create_activity) + + object = Object.normalize(activity, fetch: false) + + assert activity.data["context"] == object.data["context"] + end + + test "it keeps link tags" do + insert(:user, ap_id: "https://example.org/users/alice") + + message = File.read!("test/fixtures/fep-e232.json") |> Jason.decode!() + + assert capture_log(fn -> + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + object = Object.normalize(activity) + assert [%{"type" => "Mention"}, %{"type" => "Link"}] = object.data["tag"] + end) =~ "Object rejected while fetching" + end + + test "it accepts quote posts" do + insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i") + + object = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!() + + message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "actor" => "https://misskey.io/users/7rkrarq81i", + "object" => object + } + + assert {:ok, activity} = Transmogrifier.handle_incoming(message) + + # Object was created in the database + object = Object.normalize(activity) + assert object.data["quoteUrl"] == "https://misskey.io/notes/8vs6wxufd0" + + # It fetched the quoted post + assert Object.normalize("https://misskey.io/notes/8vs6wxufd0") end end @@ -227,7 +265,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert is_nil(modified["object"]["like_count"]) assert is_nil(modified["object"]["announcements"]) assert is_nil(modified["object"]["announcement_count"]) - assert is_nil(modified["object"]["context_id"]) assert is_nil(modified["object"]["generator"]) end @@ -242,7 +279,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert is_nil(modified["object"]["like_count"]) assert is_nil(modified["object"]["announcements"]) assert is_nil(modified["object"]["announcement_count"]) - assert is_nil(modified["object"]["context_id"]) assert is_nil(modified["object"]["likes"]) end @@ -312,68 +348,41 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert url == "http://localhost:4001/emoji/dino%20walking.gif" end - end - - describe "user upgrade" do - test "it upgrades a user to activitypub" do - user = - insert(:user, %{ - nickname: "rye@niu.moe", - local: false, - ap_id: "https://niu.moe/users/rye", - follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"}) - }) - - user_two = insert(:user) - Pleroma.FollowingRelationship.follow(user_two, user, :follow_accept) - {:ok, activity} = CommonAPI.post(user, %{status: "test"}) - {:ok, unrelated_activity} = CommonAPI.post(user_two, %{status: "test"}) - assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients - - user = User.get_cached_by_id(user.id) - assert user.note_count == 1 - - {:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye") - ObanHelpers.perform_all() - - assert user.ap_enabled - assert user.note_count == 1 - assert user.follower_address == "https://niu.moe/users/rye/followers" - assert user.following_address == "https://niu.moe/users/rye/following" + test "Updates of Notes are handled" do + user = insert(:user) - user = User.get_cached_by_id(user.id) - assert user.note_count == 1 + {:ok, activity} = CommonAPI.post(user, %{status: "everybody do the dinosaur :dinosaur:"}) + {:ok, update} = CommonAPI.update(user, activity, %{status: "mew mew :blank:"}) - activity = Activity.get_by_id(activity.id) - assert user.follower_address in activity.recipients + {:ok, prepared} = Transmogrifier.prepare_outgoing(update.data) assert %{ - "url" => [ - %{ - "href" => - "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg" - } - ] - } = user.avatar + "content" => "mew mew :blank:", + "tag" => [%{"name" => ":blank:", "type" => "Emoji"}], + "formerRepresentations" => %{ + "orderedItems" => [ + %{ + "content" => "everybody do the dinosaur :dinosaur:", + "tag" => [%{"name" => ":dinosaur:", "type" => "Emoji"}] + } + ] + } + } = prepared["object"] + end - assert %{ - "url" => [ - %{ - "href" => - "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png" - } - ] - } = user.banner + test "it prepares a quote post" do + user = insert(:user) + + {:ok, quoted_post} = CommonAPI.post(user, %{status: "hey"}) + {:ok, quote_post} = CommonAPI.post(user, %{status: "hey", quote_id: quoted_post.id}) - refute "..." in activity.recipients + {:ok, modified} = Transmogrifier.prepare_outgoing(quote_post.data) - unrelated_activity = Activity.get_by_id(unrelated_activity.id) - refute user.follower_address in unrelated_activity.recipients + %{data: %{"id" => quote_id}} = Object.normalize(quoted_post) - user_two = User.get_cached_by_id(user_two.id) - assert User.following?(user_two, user) - refute "..." in User.following(user_two) + assert modified["object"]["quoteUrl"] == quote_id + assert modified["object"]["quoteUri"] == quote_id end end @@ -401,7 +410,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert capture_log(fn -> {:error, _} = Transmogrifier.handle_incoming(data) - end) =~ "Object containment failed" + end) =~ "Object rejected while fetching" end test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do @@ -416,7 +425,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert capture_log(fn -> {:error, _} = Transmogrifier.handle_incoming(data) - end) =~ "Object containment failed" + end) =~ "Object rejected while fetching" end test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do @@ -431,7 +440,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert capture_log(fn -> {:error, _} = Transmogrifier.handle_incoming(data) - end) =~ "Object containment failed" + end) =~ "Object rejected while fetching" end end @@ -575,4 +584,43 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do assert Transmogrifier.fix_attachments(object) == expected end end + + describe "prepare_object/1" do + test "it processes history" do + original = %{ + "formerRepresentations" => %{ + "orderedItems" => [ + %{ + "generator" => %{}, + "emoji" => %{"blobcat" => "http://localhost:4001/emoji/blobcat.png"} + } + ] + } + } + + processed = Transmogrifier.prepare_object(original) + + history_item = Enum.at(processed["formerRepresentations"]["orderedItems"], 0) + + refute Map.has_key?(history_item, "generator") + + assert [%{"name" => ":blobcat:"}] = history_item["tag"] + end + + test "it works when there is no or bad history" do + original = %{ + "formerRepresentations" => %{ + "items" => [ + %{ + "generator" => %{}, + "emoji" => %{"blobcat" => "http://localhost:4001/emoji/blobcat.png"} + } + ] + } + } + + processed = Transmogrifier.prepare_object(original) + assert processed["formerRepresentations"] == original["formerRepresentations"] + end + end end diff --git a/test/pleroma/web/activity_pub/utils_test.exs b/test/pleroma/web/activity_pub/utils_test.exs index 447621718..cd61e3e4b 100644 --- a/test/pleroma/web/activity_pub/utils_test.exs +++ b/test/pleroma/web/activity_pub/utils_test.exs @@ -16,6 +16,41 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do require Pleroma.Constants + describe "strip_report_status_data/1" do + test "does not break on issues with the reported activities" do + reporter = insert(:user) + target_account = insert(:user) + {:ok, activity} = CommonAPI.post(target_account, %{status: "foobar"}) + context = Utils.generate_context_id() + content = "foobar" + post_id = activity.data["id"] + + res = + Utils.make_flag_data( + %{ + actor: reporter, + context: context, + account: target_account, + statuses: [%{"id" => post_id}], + content: content + }, + %{} + ) + + res = + res + |> Map.put("object", res["object"] ++ [nil, 1, 5, "123"]) + + {:ok, activity} = Pleroma.Web.ActivityPub.ActivityPub.insert(res) + + [user_id, object | _] = activity.data["object"] + + {:ok, stripped} = Utils.strip_report_status_data(activity) + + assert stripped.data["object"] == [user_id, object["id"]] + end + end + describe "fetch the latest Follow" do test "fetches the latest Follow activity" do %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity) @@ -118,7 +153,7 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do assert Enum.sort(cc) == expected_cc end - test "does not adress actor's follower address if the activity is not public", %{ + test "does not address actor's follower address if the activity is not public", %{ user: user, other_user: other_user, third_user: third_user @@ -429,7 +464,6 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do object = Object.normalize(note_activity, fetch: false) res = Utils.lazy_put_activity_defaults(%{"context" => object.data["id"]}) assert res["context"] == object.data["id"] - assert res["context_id"] == object.id assert res["id"] assert res["published"] end @@ -437,7 +471,6 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do test "returns map with fake id and published data" do assert %{ "context" => "pleroma:fakecontext", - "context_id" => -1, "id" => "pleroma:fakeid", "published" => _ } = Utils.lazy_put_activity_defaults(%{}, true) @@ -454,13 +487,11 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do }) assert res["context"] == object.data["id"] - assert res["context_id"] == object.id assert res["id"] assert res["published"] assert res["object"]["id"] assert res["object"]["published"] assert res["object"]["context"] == object.data["id"] - assert res["object"]["context_id"] == object.id end end @@ -477,7 +508,7 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do content = "foobar" target_ap_id = target_account.ap_id - activity_ap_id = activity.data["id"] + object_ap_id = activity.object.data["id"] res = Utils.make_flag_data( @@ -493,7 +524,7 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do note_obj = %{ "type" => "Note", - "id" => activity_ap_id, + "id" => object_ap_id, "content" => content, "published" => activity.object.data["published"], "actor" => @@ -508,6 +539,49 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do "state" => "open" } = res end + + test "returns map with Flag object with a non-Create Activity" do + reporter = insert(:user) + posting_account = insert(:user) + target_account = insert(:user) + + {:ok, activity} = CommonAPI.post(posting_account, %{status: "foobar"}) + {:ok, like} = CommonAPI.favorite(target_account, activity.id) + context = Utils.generate_context_id() + content = "foobar" + + target_ap_id = target_account.ap_id + object_ap_id = activity.object.data["id"] + + res = + Utils.make_flag_data( + %{ + actor: reporter, + context: context, + account: target_account, + statuses: [%{"id" => like.data["id"]}], + content: content + }, + %{} + ) + + note_obj = %{ + "type" => "Note", + "id" => object_ap_id, + "content" => content, + "published" => activity.object.data["published"], + "actor" => + AccountView.render("show.json", %{user: posting_account, skip_visibility_check: true}) + } + + assert %{ + "type" => "Flag", + "content" => ^content, + "context" => ^context, + "object" => [^target_ap_id, ^note_obj], + "state" => "open" + } = res + end end describe "add_announce_to_object/2" do @@ -548,15 +622,38 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do end describe "get_cached_emoji_reactions/1" do - test "returns the data or an emtpy list" do + test "returns the normalized data or an empty list" do object = insert(:note) assert Utils.get_cached_emoji_reactions(object) == [] object = insert(:note, data: %{"reactions" => [["x", ["lain"]]]}) - assert Utils.get_cached_emoji_reactions(object) == [["x", ["lain"]]] + assert Utils.get_cached_emoji_reactions(object) == [["x", ["lain"], nil]] object = insert(:note, data: %{"reactions" => %{}}) assert Utils.get_cached_emoji_reactions(object) == [] end end + + describe "add_emoji_reaction_to_object/1" do + test "works with legacy 2-tuple format" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + note = + insert(:note, + user: user, + data: %{ + "reactions" => [["😿", [other_user.ap_id]]] + } + ) + + _activity = insert(:note_activity, user: user, note: note) + + Utils.add_emoji_reaction_to_object( + %Activity{data: %{"content" => "😿", "actor" => third_user.ap_id}}, + note + ) + end + end end diff --git a/test/pleroma/web/activity_pub/views/object_view_test.exs b/test/pleroma/web/activity_pub/views/object_view_test.exs index 48a4b47c4..d94878e31 100644 --- a/test/pleroma/web/activity_pub/views/object_view_test.exs +++ b/test/pleroma/web/activity_pub/views/object_view_test.exs @@ -81,4 +81,18 @@ defmodule Pleroma.Web.ActivityPub.ObjectViewTest do assert result["object"] == object.data["id"] assert result["type"] == "Announce" end + + test "renders an undo announce activity" do + note = insert(:note_activity) + user = insert(:user) + + {:ok, announce} = CommonAPI.repeat(note.id, user) + {:ok, undo} = CommonAPI.unrepeat(note.id, user) + + result = ObjectView.render("object.json", %{object: undo}) + + assert result["id"] == undo.data["id"] + assert result["object"] == announce.data["id"] + assert result["type"] == "Undo" + end end diff --git a/test/pleroma/web/activity_pub/views/user_view_test.exs b/test/pleroma/web/activity_pub/views/user_view_test.exs index 5cbfd8ab7..c75149dab 100644 --- a/test/pleroma/web/activity_pub/views/user_view_test.exs +++ b/test/pleroma/web/activity_pub/views/user_view_test.exs @@ -12,7 +12,6 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do test "Renders a user, including the public key" do user = insert(:user) - {:ok, user} = User.ensure_keys_present(user) result = UserView.render("user.json", %{user: user}) @@ -55,7 +54,6 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do test "Does not add an avatar image if the user hasn't set one" do user = insert(:user) - {:ok, user} = User.ensure_keys_present(user) result = UserView.render("user.json", %{user: user}) refute result["icon"] @@ -67,8 +65,6 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do banner: %{"url" => [%{"href" => "https://somebanner"}]} ) - {:ok, user} = User.ensure_keys_present(user) - result = UserView.render("user.json", %{user: user}) assert result["icon"]["url"] == "https://someurl" assert result["image"]["url"] == "https://somebanner" @@ -80,16 +76,31 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do assert %{"invisible" => true} = UserView.render("service.json", %{user: user}) end + test "service has a few essential fields" do + user = insert(:user) + result = UserView.render("service.json", %{user: user}) + assert result["id"] + assert result["type"] == "Application" + assert result["inbox"] + assert result["outbox"] + end + test "renders AKAs" do akas = ["https://i.tusooa.xyz/users/test-pleroma"] user = insert(:user, also_known_as: akas) assert %{"alsoKnownAs" => ^akas} = UserView.render("user.json", %{user: user}) end + test "renders full nickname" do + clear_config([Pleroma.Web.WebFinger, :domain], "plemora.dev") + + user = insert(:user, nickname: "user") + assert %{"webfinger" => "acct:user@plemora.dev"} = UserView.render("user.json", %{user: user}) + end + describe "endpoints" do test "local users have a usable endpoints structure" do user = insert(:user) - {:ok, user} = User.ensure_keys_present(user) result = UserView.render("user.json", %{user: user}) @@ -105,7 +116,6 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do test "remote users have an empty endpoints structure" do user = insert(:user, local: false) - {:ok, user} = User.ensure_keys_present(user) result = UserView.render("user.json", %{user: user}) @@ -115,7 +125,6 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do test "instance users do not expose oAuth endpoints" do user = insert(:user, nickname: nil, local: true) - {:ok, user} = User.ensure_keys_present(user) result = UserView.render("user.json", %{user: user}) diff --git a/test/pleroma/web/activity_pub/visibility_test.exs b/test/pleroma/web/activity_pub/visibility_test.exs index 8c4c06a95..fd3dc83a1 100644 --- a/test/pleroma/web/activity_pub/visibility_test.exs +++ b/test/pleroma/web/activity_pub/visibility_test.exs @@ -52,60 +52,60 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do } end - test "is_direct?", %{ + test "direct?", %{ public: public, private: private, direct: direct, unlisted: unlisted, list: list } do - assert Visibility.is_direct?(direct) - refute Visibility.is_direct?(public) - refute Visibility.is_direct?(private) - refute Visibility.is_direct?(unlisted) - assert Visibility.is_direct?(list) + assert Visibility.direct?(direct) + refute Visibility.direct?(public) + refute Visibility.direct?(private) + refute Visibility.direct?(unlisted) + assert Visibility.direct?(list) end - test "is_public?", %{ + test "public?", %{ public: public, private: private, direct: direct, unlisted: unlisted, list: list } do - refute Visibility.is_public?(direct) - assert Visibility.is_public?(public) - refute Visibility.is_public?(private) - assert Visibility.is_public?(unlisted) - refute Visibility.is_public?(list) + refute Visibility.public?(direct) + assert Visibility.public?(public) + refute Visibility.public?(private) + assert Visibility.public?(unlisted) + refute Visibility.public?(list) end - test "is_private?", %{ + test "private?", %{ public: public, private: private, direct: direct, unlisted: unlisted, list: list } do - refute Visibility.is_private?(direct) - refute Visibility.is_private?(public) - assert Visibility.is_private?(private) - refute Visibility.is_private?(unlisted) - refute Visibility.is_private?(list) + refute Visibility.private?(direct) + refute Visibility.private?(public) + assert Visibility.private?(private) + refute Visibility.private?(unlisted) + refute Visibility.private?(list) end - test "is_list?", %{ + test "list?", %{ public: public, private: private, direct: direct, unlisted: unlisted, list: list } do - refute Visibility.is_list?(direct) - refute Visibility.is_list?(public) - refute Visibility.is_list?(private) - refute Visibility.is_list?(unlisted) - assert Visibility.is_list?(list) + refute Visibility.list?(direct) + refute Visibility.list?(public) + refute Visibility.list?(private) + refute Visibility.list?(unlisted) + assert Visibility.list?(list) end test "visible_for_user? Activity", %{ @@ -227,7 +227,7 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do } do Repo.delete(user) Pleroma.User.invalidate_cache(user) - refute Visibility.is_private?(direct) + refute Visibility.private?(direct) end test "get_visibility", %{ diff --git a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs index d83f7f011..a7ee8359d 100644 --- a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do - use Pleroma.Web.ConnCase + use Pleroma.Web.ConnCase, async: false use Oban.Testing, repo: Pleroma.Repo import ExUnit.CaptureLog @@ -15,6 +15,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do alias Pleroma.ModerationLog alias Pleroma.Repo alias Pleroma.Tests.ObanHelpers + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.User alias Pleroma.Web.CommonAPI @@ -92,18 +93,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do describe "PUT /api/pleroma/admin/users/tag" do setup %{conn: conn} do + clear_config([:instance, :admin_privileges], [:users_manage_tags]) + user1 = insert(:user, %{tags: ["x"]}) user2 = insert(:user, %{tags: ["y"]}) user3 = insert(:user, %{tags: ["unchanged"]}) - conn = - conn - |> put_req_header("accept", "application/json") - |> put( - "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <> - "#{user2.nickname}&tags[]=foo&tags[]=bar" - ) - %{conn: conn, user1: user1, user2: user2, user3: user3} end @@ -113,6 +108,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do user1: user1, user2: user2 } do + conn = + conn + |> put_req_header("accept", "application/json") + |> put( + "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <> + "#{user2.nickname}&tags[]=foo&tags[]=bar" + ) + assert empty_json_response(conn) assert User.get_cached_by_id(user1.id).tags == ["x", "foo", "bar"] assert User.get_cached_by_id(user2.id).tags == ["y", "foo", "bar"] @@ -130,26 +133,43 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do "@#{admin.nickname} added tags: #{tags} to users: #{users}" end - test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do + test "it does not modify tags of not specified users", %{ + conn: conn, + user1: user1, + user2: user2, + user3: user3 + } do + conn = + conn + |> put_req_header("accept", "application/json") + |> put( + "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <> + "#{user2.nickname}&tags[]=foo&tags[]=bar" + ) + assert empty_json_response(conn) assert User.get_cached_by_id(user3.id).tags == ["unchanged"] end + + test "it requires privileged role :users_manage_tags", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + response = + conn + |> put_req_header("accept", "application/json") + |> put("/api/pleroma/admin/users/tag?nicknames[]=nickname&tags[]=foo&tags[]=bar") + + assert json_response(response, :forbidden) + end end describe "DELETE /api/pleroma/admin/users/tag" do setup %{conn: conn} do + clear_config([:instance, :admin_privileges], [:users_manage_tags]) user1 = insert(:user, %{tags: ["x"]}) user2 = insert(:user, %{tags: ["y", "z"]}) user3 = insert(:user, %{tags: ["unchanged"]}) - conn = - conn - |> put_req_header("accept", "application/json") - |> delete( - "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <> - "#{user2.nickname}&tags[]=x&tags[]=z" - ) - %{conn: conn, user1: user1, user2: user2, user3: user3} end @@ -159,6 +179,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do user1: user1, user2: user2 } do + conn = + conn + |> put_req_header("accept", "application/json") + |> delete( + "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <> + "#{user2.nickname}&tags[]=x&tags[]=z" + ) + assert empty_json_response(conn) assert User.get_cached_by_id(user1.id).tags == [] assert User.get_cached_by_id(user2.id).tags == ["y"] @@ -176,10 +204,34 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do "@#{admin.nickname} removed tags: #{tags} from users: #{users}" end - test "it does not modify tags of not specified users", %{conn: conn, user3: user3} do + test "it does not modify tags of not specified users", %{ + conn: conn, + user1: user1, + user2: user2, + user3: user3 + } do + conn = + conn + |> put_req_header("accept", "application/json") + |> delete( + "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <> + "#{user2.nickname}&tags[]=x&tags[]=z" + ) + assert empty_json_response(conn) assert User.get_cached_by_id(user3.id).tags == ["unchanged"] end + + test "it requires privileged role :users_manage_tags", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + response = + conn + |> put_req_header("accept", "application/json") + |> delete("/api/pleroma/admin/users/tag?nicknames[]=nickname&tags[]=foo&tags[]=bar") + + assert json_response(response, :forbidden) + end end describe "/api/pleroma/admin/users/:nickname/permission_group" do @@ -271,21 +323,38 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do end end - test "/api/pleroma/admin/users/:nickname/password_reset", %{conn: conn} do - user = insert(:user) + describe "/api/pleroma/admin/users/:nickname/password_reset" do + test "it returns a password reset link", %{conn: conn} do + clear_config([:instance, :admin_privileges], [:users_manage_credentials]) - conn = - conn - |> put_req_header("accept", "application/json") - |> get("/api/pleroma/admin/users/#{user.nickname}/password_reset") + user = insert(:user) + + conn = + conn + |> put_req_header("accept", "application/json") + |> get("/api/pleroma/admin/users/#{user.nickname}/password_reset") + + resp = json_response(conn, 200) - resp = json_response(conn, 200) + assert Regex.match?(~r/(http:\/\/|https:\/\/)/, resp["link"]) + end + + test "it requires privileged role :users_manage_credentials", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + response = + conn + |> put_req_header("accept", "application/json") + |> get("/api/pleroma/admin/users/nickname/password_reset") - assert Regex.match?(~r/(http:\/\/|https:\/\/)/, resp["link"]) + assert json_response(response, :forbidden) + end end describe "PUT disable_mfa" do test "returns 200 and disable 2fa", %{conn: conn} do + clear_config([:instance, :admin_privileges], [:users_manage_credentials]) + user = insert(:user, multi_factor_authentication_settings: %MFA.Settings{ @@ -307,6 +376,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do end test "returns 404 if user not found", %{conn: conn} do + clear_config([:instance, :admin_privileges], [:users_manage_credentials]) + response = conn |> put("/api/pleroma/admin/users/disable_mfa", %{nickname: "nickname"}) @@ -314,6 +385,16 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert response == %{"error" => "Not found"} end + + test "it requires privileged role :users_manage_credentials", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + response = + conn + |> put("/api/pleroma/admin/users/disable_mfa", %{nickname: "nickname"}) + + assert json_response(response, :forbidden) + end end describe "GET /api/pleroma/admin/restart" do @@ -344,6 +425,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do describe "GET /api/pleroma/admin/users/:nickname/statuses" do setup do + clear_config([:instance, :admin_privileges], [:messages_read]) + user = insert(:user) insert(:note_activity, user: user) @@ -360,6 +443,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert length(activities) == 3 end + test "it requires privileged role :messages_read", %{conn: conn, user: user} do + clear_config([:instance, :admin_privileges], []) + + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/statuses") + + assert json_response(conn, :forbidden) + end + test "renders user's statuses with pagination", %{conn: conn, user: user} do %{"total" => 3, "activities" => [activity1]} = conn @@ -421,20 +512,31 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do describe "GET /api/pleroma/admin/users/:nickname/chats" do setup do + clear_config([:instance, :admin_privileges], [:messages_read]) + user = insert(:user) + + %{user: user} + end + + test "renders user's chats", %{conn: conn, user: user} do recipients = insert_list(3, :user) Enum.each(recipients, fn recipient -> CommonAPI.post_chat_message(user, recipient, "yo") end) - %{user: user} + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/chats") + + assert json_response(conn, 200) |> length() == 3 end - test "renders user's chats", %{conn: conn, user: user} do + test "it requires privileged role :messages_read", %{conn: conn, user: user} do + clear_config([:instance, :admin_privileges], []) + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/chats") - assert json_response(conn, 200) |> length() == 3 + assert json_response(conn, :forbidden) end end @@ -471,6 +573,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do describe "GET /api/pleroma/admin/moderation_log" do setup do + clear_config([:instance, :admin_privileges], [:moderation_log_read]) moderator = insert(:user, is_moderator: true) %{moderator: moderator} @@ -675,6 +778,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert get_in(first_entry, ["data", "message"]) == "@#{moderator.nickname} unfollowed relay: https://example.org/relay" end + + test "it requires privileged role :moderation_log_read", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + assert conn + |> put_req_header("content-type", "multipart/form-data") + |> get("/api/pleroma/admin/moderation_log") + |> json_response(:forbidden) + end end test "gets a remote users when [:instance, :limit_to_local_content] is set to :unauthenticated", @@ -688,6 +800,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do describe "GET /users/:nickname/credentials" do test "gets the user credentials", %{conn: conn} do + clear_config([:instance, :admin_privileges], [:users_manage_credentials]) user = insert(:user) conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials") @@ -696,6 +809,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do end test "returns 403 if requested by a non-admin" do + clear_config([:instance, :admin_privileges], [:users_manage_credentials]) user = insert(:user) conn = @@ -705,6 +819,16 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert json_response(conn, :forbidden) end + + test "it requires privileged role :users_manage_credentials", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + response = + conn + |> get("/api/pleroma/admin/users/nickname/credentials") + + assert json_response(response, :forbidden) + end end describe "PATCH /users/:nickname/credentials" do @@ -714,6 +838,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do end test "changes password and email", %{conn: conn, admin: admin, user: user} do + clear_config([:instance, :admin_privileges], [:users_manage_credentials]) + assert user.password_reset_pending == false conn = @@ -756,6 +882,19 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert json_response(conn, :forbidden) end + test "returns 403 if not privileged with :users_manage_credentials", %{conn: conn, user: user} do + clear_config([:instance, :admin_privileges], []) + + conn = + patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{ + "password" => "new_password", + "email" => "new_email@example.com", + "name" => "new_name" + }) + + assert json_response(conn, :forbidden) + end + test "changes actor type from permitted list", %{conn: conn, user: user} do assert user.actor_type == "Person" @@ -784,6 +923,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do describe "PATCH /users/:nickname/force_password_reset" do test "sets password_reset_pending to true", %{conn: conn} do + clear_config([:instance, :admin_privileges], [:users_manage_credentials]) user = insert(:user) assert user.password_reset_pending == false @@ -796,10 +936,21 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert User.get_by_id(user.id).password_reset_pending == true end + + test "it requires privileged role :users_manage_credentials", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + response = + conn + |> patch("/api/pleroma/admin/users/force_password_reset", %{nickname: "nickname"}) + + assert json_response(response, :forbidden) + end end describe "PATCH /confirm_email" do test "it confirms emails of two users", %{conn: conn, admin: admin} do + clear_config([:instance, :admin_privileges], [:users_manage_credentials]) [first_user, second_user] = insert_pair(:user, is_confirmed: false) refute first_user.is_confirmed @@ -826,10 +977,21 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert ModerationLog.get_log_entry_message(log_entry) == "@#{admin.nickname} confirmed email for users: @#{first_user.nickname}, @#{second_user.nickname}" end + + test "it requires privileged role :users_manage_credentials", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + response = + conn + |> patch("/api/pleroma/admin/users/confirm_email", %{nicknames: ["nickname"]}) + + assert json_response(response, :forbidden) + end end describe "PATCH /resend_confirmation_email" do test "it resend emails for two users", %{conn: conn, admin: admin} do + clear_config([:instance, :admin_privileges], [:users_manage_credentials]) [first_user, second_user] = insert_pair(:user, is_confirmed: false) ret_conn = @@ -855,9 +1017,23 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do |> Swoosh.Email.put_private(:hackney_options, ssl_options: [versions: [:"tlsv1.2"]]) |> assert_email_sent() end + + test "it requires privileged role :users_manage_credentials", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + response = + conn + |> patch("/api/pleroma/admin/users/resend_confirmation_email", %{nicknames: ["nickname"]}) + + assert json_response(response, :forbidden) + end end describe "/api/pleroma/admin/stats" do + setup do + clear_config([:instance, :admin_privileges], [:statistics_read]) + end + test "status visibility count", %{conn: conn} do user = insert(:user) CommonAPI.post(user, %{visibility: "public", status: "hey"}) @@ -890,10 +1066,21 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert %{"direct" => 0, "private" => 1, "public" => 0, "unlisted" => 1} = response["status_visibility"] end + + test "it requires privileged role :statistics_read", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + assert conn + |> get("/api/pleroma/admin/stats", instance: "lain.wired") + |> json_response(:forbidden) + end end describe "/api/pleroma/backups" do test "it creates a backup", %{conn: conn} do + ConfigMock + |> Mox.stub_with(Pleroma.Config) + admin = %{id: admin_id, nickname: admin_nickname} = insert(:user, is_admin: true) token = insert(:oauth_admin_token, user: admin) user = %{id: user_id, nickname: user_nickname} = insert(:user) @@ -958,6 +1145,34 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do assert Repo.aggregate(Pleroma.User.Backup, :count) == 2 end end + + describe "POST /api/v1/pleroma/admin/reload_emoji" do + setup do + clear_config([:instance, :admin_privileges], [:emoji_manage_emoji]) + + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{conn: conn, admin: admin}} + end + + test "it requires privileged role :emoji_manage_emoji", %{conn: conn} do + assert conn + |> post("/api/v1/pleroma/admin/reload_emoji") + |> json_response(200) + + clear_config([:instance, :admin_privileges], []) + + assert conn + |> post("/api/v1/pleroma/admin/reload_emoji") + |> json_response(:forbidden) + end + end end # Needed for testing diff --git a/test/pleroma/web/admin_api/controllers/announcement_controller_test.exs b/test/pleroma/web/admin_api/controllers/announcement_controller_test.exs index 5b8148c05..cf60bcad5 100644 --- a/test/pleroma/web/admin_api/controllers/announcement_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/announcement_controller_test.exs @@ -3,11 +3,12 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.AdminAPI.AnnouncementControllerTest do - use Pleroma.Web.ConnCase + use Pleroma.Web.ConnCase, async: false import Pleroma.Factory setup do + clear_config([:instance, :admin_privileges], [:announcements_manage_announcements]) admin = insert(:user, is_admin: true) token = insert(:oauth_admin_token, user: admin) @@ -31,6 +32,18 @@ defmodule Pleroma.Web.AdminAPI.AnnouncementControllerTest do assert [%{"id" => ^id}] = response end + test "it requires privileged role :announcements_manage_announcements", %{conn: conn} do + conn + |> get("/api/v1/pleroma/admin/announcements") + |> json_response_and_validate_schema(:ok) + + clear_config([:instance, :admin_privileges], []) + + conn + |> get("/api/v1/pleroma/admin/announcements") + |> json_response(:forbidden) + end + test "it paginates announcements", %{conn: conn} do _announcements = Enum.map(0..20, fn _ -> insert(:announcement) end) @@ -92,6 +105,20 @@ defmodule Pleroma.Web.AdminAPI.AnnouncementControllerTest do assert %{"id" => ^id} = response end + test "it requires privileged role :announcements_manage_announcements", %{conn: conn} do + %{id: id} = insert(:announcement) + + conn + |> get("/api/v1/pleroma/admin/announcements/#{id}") + |> json_response_and_validate_schema(:ok) + + clear_config([:instance, :admin_privileges], []) + + conn + |> get("/api/v1/pleroma/admin/announcements/#{id}") + |> json_response(:forbidden) + end + test "it returns not found for non-existent id", %{conn: conn} do %{id: id} = insert(:announcement) @@ -112,6 +139,20 @@ defmodule Pleroma.Web.AdminAPI.AnnouncementControllerTest do |> json_response_and_validate_schema(:ok) end + test "it requires privileged role :announcements_manage_announcements", %{conn: conn} do + %{id: id} = insert(:announcement) + + conn + |> delete("/api/v1/pleroma/admin/announcements/#{id}") + |> json_response_and_validate_schema(:ok) + + clear_config([:instance, :admin_privileges], []) + + conn + |> delete("/api/v1/pleroma/admin/announcements/#{id}") + |> json_response(:forbidden) + end + test "it returns not found for non-existent id", %{conn: conn} do %{id: id} = insert(:announcement) @@ -156,6 +197,29 @@ defmodule Pleroma.Web.AdminAPI.AnnouncementControllerTest do assert NaiveDateTime.compare(new.starts_at, starts_at) == :eq end + test "it requires privileged role :announcements_manage_announcements", %{conn: conn} do + %{id: id} = insert(:announcement) + + now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) + starts_at = NaiveDateTime.add(now, -10, :second) + + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/v1/pleroma/admin/announcements/#{id}", %{ + starts_at: NaiveDateTime.to_iso8601(starts_at) + }) + |> json_response_and_validate_schema(:ok) + + clear_config([:instance, :admin_privileges], []) + + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/v1/pleroma/admin/announcements/#{id}", %{ + starts_at: NaiveDateTime.to_iso8601(starts_at) + }) + |> json_response(:forbidden) + end + test "it updates with time with utc timezone", %{conn: conn} do %{id: id} = insert(:announcement) @@ -250,6 +314,36 @@ defmodule Pleroma.Web.AdminAPI.AnnouncementControllerTest do assert NaiveDateTime.compare(announcement.ends_at, ends_at) == :eq end + test "it requires privileged role :announcements_manage_announcements", %{conn: conn} do + content = "test post announcement api" + + now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) + starts_at = NaiveDateTime.add(now, -10, :second) + ends_at = NaiveDateTime.add(now, 10, :second) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/admin/announcements", %{ + "content" => content, + "starts_at" => NaiveDateTime.to_iso8601(starts_at), + "ends_at" => NaiveDateTime.to_iso8601(ends_at), + "all_day" => true + }) + |> json_response_and_validate_schema(:ok) + + clear_config([:instance, :admin_privileges], []) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/admin/announcements", %{ + "content" => content, + "starts_at" => NaiveDateTime.to_iso8601(starts_at), + "ends_at" => NaiveDateTime.to_iso8601(ends_at), + "all_day" => true + }) + |> json_response(:forbidden) + end + test "creating with time with utc timezones", %{conn: conn} do content = "test post announcement api" diff --git a/test/pleroma/web/admin_api/controllers/chat_controller_test.exs b/test/pleroma/web/admin_api/controllers/chat_controller_test.exs index ccf25a244..1b5c31b7d 100644 --- a/test/pleroma/web/admin_api/controllers/chat_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/chat_controller_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.AdminAPI.ChatControllerTest do - use Pleroma.Web.ConnCase, async: true + use Pleroma.Web.ConnCase, async: false import Pleroma.Factory @@ -27,7 +27,10 @@ defmodule Pleroma.Web.AdminAPI.ChatControllerTest do end describe "DELETE /api/pleroma/admin/chats/:id/messages/:message_id" do - setup do: admin_setup() + setup do + clear_config([:instance, :admin_privileges], [:messages_delete]) + admin_setup() + end test "it deletes a message from the chat", %{conn: conn, admin: admin} do user = insert(:user) @@ -53,17 +56,29 @@ defmodule Pleroma.Web.AdminAPI.ChatControllerTest do log_entry = Repo.one(ModerationLog) assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} deleted chat message ##{cm_ref.id}" + "@#{admin.nickname} deleted chat message ##{message.id}" assert result["id"] == cm_ref.id refute MessageReference.get_by_id(cm_ref.id) refute MessageReference.get_by_id(recipient_cm_ref.id) assert %{data: %{"type" => "Tombstone"}} = Object.get_by_id(object.id) end + + test "it requires privileged role :messages_delete", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + assert conn + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/admin/chats/some_id/messages/some_ref_id") + |> json_response(:forbidden) + end end describe "GET /api/pleroma/admin/chats/:id/messages" do - setup do: admin_setup() + setup do + clear_config([:instance, :admin_privileges], [:messages_read]) + admin_setup() + end test "it paginates", %{conn: conn} do user = insert(:user) @@ -114,10 +129,21 @@ defmodule Pleroma.Web.AdminAPI.ChatControllerTest do assert length(result) == 3 end + + test "it requires privileged role :messages_read", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + conn = get(conn, "/api/pleroma/admin/chats/some_id/messages") + + assert json_response(conn, :forbidden) + end end describe "GET /api/pleroma/admin/chats/:id" do - setup do: admin_setup() + setup do + clear_config([:instance, :admin_privileges], [:messages_read]) + admin_setup() + end test "it returns a chat", %{conn: conn} do user = insert(:user) @@ -135,6 +161,14 @@ defmodule Pleroma.Web.AdminAPI.ChatControllerTest do assert %{} = result["receiver"] refute result["account"] end + + test "it requires privileged role :messages_read", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + conn = get(conn, "/api/pleroma/admin/chats/some_id") + + assert json_response(conn, :forbidden) + end end describe "unauthorized chat moderation" do diff --git a/test/pleroma/web/admin_api/controllers/config_controller_test.exs b/test/pleroma/web/admin_api/controllers/config_controller_test.exs index 6d014b65b..734aca752 100644 --- a/test/pleroma/web/admin_api/controllers/config_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/config_controller_test.exs @@ -316,15 +316,16 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do assert Application.get_env(:idna, :key5) == {"string", Pleroma.Captcha.NotReal, []} end + @tag capture_log: true test "save configs setting without explicit key", %{conn: conn} do - level = Application.get_env(:quack, :level) - meta = Application.get_env(:quack, :meta) - webhook_url = Application.get_env(:quack, :webhook_url) + adapter = Application.get_env(:http, :adapter) + send_user_agent = Application.get_env(:http, :send_user_agent) + user_agent = Application.get_env(:http, :user_agent) on_exit(fn -> - Application.put_env(:quack, :level, level) - Application.put_env(:quack, :meta, meta) - Application.put_env(:quack, :webhook_url, webhook_url) + Application.put_env(:http, :adapter, adapter) + Application.put_env(:http, :send_user_agent, send_user_agent) + Application.put_env(:http, :user_agent, user_agent) end) conn = @@ -333,19 +334,19 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do |> post("/api/pleroma/admin/config", %{ configs: [ %{ - group: ":quack", - key: ":level", - value: ":info" + group: ":http", + key: ":adapter", + value: [":someval"] }, %{ - group: ":quack", - key: ":meta", - value: [":none"] + group: ":http", + key: ":send_user_agent", + value: true }, %{ - group: ":quack", - key: ":webhook_url", - value: "https://hooks.slack.com/services/KEY" + group: ":http", + key: ":user_agent", + value: [":default"] } ] }) @@ -353,30 +354,30 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do assert json_response_and_validate_schema(conn, 200) == %{ "configs" => [ %{ - "group" => ":quack", - "key" => ":level", - "value" => ":info", - "db" => [":level"] + "group" => ":http", + "key" => ":adapter", + "value" => [":someval"], + "db" => [":adapter"] }, %{ - "group" => ":quack", - "key" => ":meta", - "value" => [":none"], - "db" => [":meta"] + "group" => ":http", + "key" => ":send_user_agent", + "value" => true, + "db" => [":send_user_agent"] }, %{ - "group" => ":quack", - "key" => ":webhook_url", - "value" => "https://hooks.slack.com/services/KEY", - "db" => [":webhook_url"] + "group" => ":http", + "key" => ":user_agent", + "value" => [":default"], + "db" => [":user_agent"] } ], "need_reboot" => false } - assert Application.get_env(:quack, :level) == :info - assert Application.get_env(:quack, :meta) == [:none] - assert Application.get_env(:quack, :webhook_url) == "https://hooks.slack.com/services/KEY" + assert Application.get_env(:http, :adapter) == [:someval] + assert Application.get_env(:http, :send_user_agent) == true + assert Application.get_env(:http, :user_agent) == [:default] end test "saving config with partial update", %{conn: conn} do @@ -872,7 +873,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do %{ "tuple" => [ ":_", - "Phoenix.Endpoint.Cowboy2Handler", + "Plug.Cowboy.Handler", %{"tuple" => ["Pleroma.Web.Endpoint", []]} ] } @@ -936,7 +937,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do %{ "tuple" => [ ":_", - "Phoenix.Endpoint.Cowboy2Handler", + "Plug.Cowboy.Handler", %{"tuple" => ["Pleroma.Web.Endpoint", []]} ] } @@ -1501,15 +1502,14 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do clear_config(:database_config_whitelist, [ {:pleroma, :instance}, {:pleroma, :activitypub}, - {:pleroma, Pleroma.Upload}, - {:esshd} + {:pleroma, Pleroma.Upload} ]) conn = get(conn, "/api/pleroma/admin/config/descriptions") children = json_response_and_validate_schema(conn, 200) - assert length(children) == 4 + assert length(children) == 3 assert Enum.count(children, fn c -> c["group"] == ":pleroma" end) == 3 @@ -1521,9 +1521,6 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do web_endpoint = Enum.find(children, fn c -> c["key"] == "Pleroma.Upload" end) assert web_endpoint["children"] - - esshd = Enum.find(children, fn c -> c["group"] == ":esshd" end) - assert esshd["children"] end end end diff --git a/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs b/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs index 38a23b224..0d1a4999e 100644 --- a/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs @@ -89,6 +89,7 @@ defmodule Pleroma.Web.AdminAPI.FrontendControllerTest do "build_url" => "http://gensokyo.2hu/builds/${ref}", "git" => nil, "installed" => true, + "installed_refs" => ["fantasy"], "name" => "pleroma", "ref" => "fantasy" } diff --git a/test/pleroma/web/admin_api/controllers/instance_controller_test.exs b/test/pleroma/web/admin_api/controllers/instance_controller_test.exs index 72436cd83..6cca623f3 100644 --- a/test/pleroma/web/admin_api/controllers/instance_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/instance_controller_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.AdminAPI.InstanceControllerTest do - use Pleroma.Web.ConnCase + use Pleroma.Web.ConnCase, async: false use Oban.Testing, repo: Pleroma.Repo import Pleroma.Factory @@ -31,6 +31,7 @@ defmodule Pleroma.Web.AdminAPI.InstanceControllerTest do end test "GET /instances/:instance/statuses", %{conn: conn} do + clear_config([:instance, :admin_privileges], [:messages_read]) user = insert(:user, local: false, ap_id: "https://archae.me/users/archaeme") user2 = insert(:user, local: false, ap_id: "https://test.com/users/test") insert_pair(:note_activity, user: user) @@ -60,9 +61,14 @@ defmodule Pleroma.Web.AdminAPI.InstanceControllerTest do |> json_response(200) assert length(activities) == 3 + + clear_config([:instance, :admin_privileges], []) + + conn |> get("/api/pleroma/admin/instances/archae.me/statuses") |> json_response(:forbidden) end test "DELETE /instances/:instance", %{conn: conn} do + clear_config([:instance, :admin_privileges], [:instances_delete]) user = insert(:user, nickname: "lain@lain.com") post = insert(:note_activity, user: user) @@ -76,5 +82,11 @@ defmodule Pleroma.Web.AdminAPI.InstanceControllerTest do assert response == "lain.com" refute Repo.reload(user).is_active refute Repo.reload(post) + + clear_config([:instance, :admin_privileges], []) + + conn + |> delete("/api/pleroma/admin/instances/lain.com") + |> json_response(:forbidden) end end diff --git a/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs b/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs index 2601a026f..9511dccea 100644 --- a/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.AdminAPI.InstanceDocumentControllerTest do - use Pleroma.Web.ConnCase, async: true + use Pleroma.Web.ConnCase import Pleroma.Factory @dir "test/tmp/instance_static" diff --git a/test/pleroma/web/admin_api/controllers/invite_controller_test.exs b/test/pleroma/web/admin_api/controllers/invite_controller_test.exs index b9d48a4b6..8051cb2e9 100644 --- a/test/pleroma/web/admin_api/controllers/invite_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/invite_controller_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.AdminAPI.InviteControllerTest do - use Pleroma.Web.ConnCase, async: true + use Pleroma.Web.ConnCase, async: false import Pleroma.Factory @@ -23,8 +23,25 @@ defmodule Pleroma.Web.AdminAPI.InviteControllerTest do end describe "POST /api/pleroma/admin/users/email_invite, with valid config" do - setup do: clear_config([:instance, :registrations_open], false) - setup do: clear_config([:instance, :invites_enabled], true) + setup do + clear_config([:instance, :registrations_open], false) + clear_config([:instance, :invites_enabled], true) + clear_config([:instance, :admin_privileges], [:users_manage_invites]) + end + + test "returns 403 if not privileged with :users_manage_invites", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + conn = + conn + |> put_req_header("content-type", "application/json;charset=utf-8") + |> post("/api/pleroma/admin/users/email_invite", %{ + email: "foo@bar.com", + name: "J. D." + }) + + assert json_response(conn, :forbidden) + end test "sends invitation and returns 204", %{admin: admin, conn: conn} do recipient_email = "foo@bar.com" @@ -114,8 +131,11 @@ defmodule Pleroma.Web.AdminAPI.InviteControllerTest do end describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do - setup do: clear_config([:instance, :registrations_open]) - setup do: clear_config([:instance, :invites_enabled]) + setup do + clear_config([:instance, :registrations_open]) + clear_config([:instance, :invites_enabled]) + clear_config([:instance, :admin_privileges], [:users_manage_invites]) + end test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn} do clear_config([:instance, :registrations_open], false) @@ -157,6 +177,21 @@ defmodule Pleroma.Web.AdminAPI.InviteControllerTest do end describe "POST /api/pleroma/admin/users/invite_token" do + setup do + clear_config([:instance, :admin_privileges], [:users_manage_invites]) + end + + test "returns 403 if not privileged with :users_manage_invites", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/invite_token") + + assert json_response(conn, :forbidden) + end + test "without options", %{conn: conn} do conn = conn @@ -221,6 +256,18 @@ defmodule Pleroma.Web.AdminAPI.InviteControllerTest do end describe "GET /api/pleroma/admin/users/invites" do + setup do + clear_config([:instance, :admin_privileges], [:users_manage_invites]) + end + + test "returns 403 if not privileged with :users_manage_invites", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + conn = get(conn, "/api/pleroma/admin/users/invites") + + assert json_response(conn, :forbidden) + end + test "no invites", %{conn: conn} do conn = get(conn, "/api/pleroma/admin/users/invites") @@ -249,6 +296,21 @@ defmodule Pleroma.Web.AdminAPI.InviteControllerTest do end describe "POST /api/pleroma/admin/users/revoke_invite" do + setup do + clear_config([:instance, :admin_privileges], [:users_manage_invites]) + end + + test "returns 403 if not privileged with :users_manage_invites", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"}) + + assert json_response(conn, :forbidden) + end + test "with token", %{conn: conn} do {:ok, invite} = UserInviteToken.create_invite() diff --git a/test/pleroma/web/admin_api/controllers/media_proxy_cache_controller_test.exs b/test/pleroma/web/admin_api/controllers/media_proxy_cache_controller_test.exs index 852334a57..de9c20145 100644 --- a/test/pleroma/web/admin_api/controllers/media_proxy_cache_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/media_proxy_cache_controller_test.exs @@ -5,9 +5,11 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheControllerTest do use Pleroma.Web.ConnCase - import Pleroma.Factory import Mock + import Mox + import Pleroma.Factory + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.Web.MediaProxy setup do: clear_config([:media_proxy]) @@ -128,6 +130,9 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheControllerTest do "http://example.com/media/fb1f4d.jpg" ] + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + with_mocks [ {MediaProxy.Invalidation.Script, [], [ @@ -150,6 +155,9 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheControllerTest do "http://example.com/media/fb1f4d.jpg" ] + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + with_mocks [{MediaProxy.Invalidation.Script, [], [purge: fn _, _ -> {"ok", 0} end]}] do conn |> put_req_header("content-type", "application/json") diff --git a/test/pleroma/web/admin_api/controllers/o_auth_app_controller_test.exs b/test/pleroma/web/admin_api/controllers/o_auth_app_controller_test.exs index 80646dd25..10eefbeca 100644 --- a/test/pleroma/web/admin_api/controllers/o_auth_app_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/o_auth_app_controller_test.exs @@ -163,7 +163,7 @@ defmodule Pleroma.Web.AdminAPI.OAuthAppControllerTest do assert response == "" end - test "with non existance id", %{conn: conn} do + test "with nonexistent id", %{conn: conn} do response = conn |> delete("/api/pleroma/admin/oauth_app/0") diff --git a/test/pleroma/web/admin_api/controllers/report_controller_test.exs b/test/pleroma/web/admin_api/controllers/report_controller_test.exs index 30dcb87e2..b626ddf55 100644 --- a/test/pleroma/web/admin_api/controllers/report_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/report_controller_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.AdminAPI.ReportControllerTest do - use Pleroma.Web.ConnCase, async: true + use Pleroma.Web.ConnCase, async: false import Pleroma.Factory @@ -11,6 +11,7 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do alias Pleroma.ModerationLog alias Pleroma.Repo alias Pleroma.ReportNote + alias Pleroma.Rule alias Pleroma.Web.CommonAPI setup do @@ -26,6 +27,20 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do end describe "GET /api/pleroma/admin/reports/:id" do + setup do + clear_config([:instance, :admin_privileges], [:reports_manage_reports]) + end + + test "returns 403 if not privileged with :reports_manage_reports", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + conn = + conn + |> get("/api/pleroma/admin/reports/report_id") + + assert json_response(conn, :forbidden) + end + test "returns report by its id", %{conn: conn} do [reporter, target_user] = insert_pair(:user) activity = insert(:note_activity, user: target_user) @@ -54,6 +69,32 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do assert notes["content"] == "this is an admin note" end + test "renders reported content even if the status is deleted", %{conn: conn} do + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + activity = Activity.normalize(activity) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + CommonAPI.delete(activity.id, target_user) + + response = + conn + |> get("/api/pleroma/admin/reports/#{report_id}") + |> json_response_and_validate_schema(:ok) + + assert response["id"] == report_id + + assert [status] = response["statuses"] + assert activity.object.data["id"] == status["uri"] + assert activity.object.data["content"] == status["content"] + end + test "returns 404 when report id is invalid", %{conn: conn} do conn = get(conn, "/api/pleroma/admin/reports/test") @@ -63,6 +104,8 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do describe "PATCH /api/pleroma/admin/reports" do setup do + clear_config([:instance, :admin_privileges], [:reports_manage_reports]) + [reporter, target_user] = insert_pair(:user) activity = insert(:note_activity, user: target_user) @@ -81,11 +124,30 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do }) %{ + reporter: reporter, id: report_id, second_report_id: second_report_id } end + test "returns 403 if not privileged with :reports_manage_reports", %{ + conn: conn, + id: id, + admin: admin + } do + clear_config([:instance, :admin_privileges], []) + + conn = + conn + |> assign(:token, insert(:oauth_token, user: admin, scopes: ["admin:write:reports"])) + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [%{"state" => "resolved", "id" => id}] + }) + + assert json_response(conn, :forbidden) + end + test "requires admin:write:reports scope", %{conn: conn, id: id, admin: admin} do read_token = insert(:oauth_token, user: admin, scopes: ["admin:read"]) write_token = insert(:oauth_token, user: admin, scopes: ["admin:write:reports"]) @@ -206,9 +268,43 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do assert ModerationLog.get_log_entry_message(second_log_entry) == "@#{admin.nickname} updated report ##{second_report_id} (on user @#{second_activity.user_actor.nickname}) with 'closed' state" end + + test "works if reporter is deactivated", %{ + conn: conn, + id: id, + reporter: reporter + } do + Pleroma.User.set_activation(reporter, false) + + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [ + %{"state" => "resolved", "id" => id} + ] + }) + |> json_response_and_validate_schema(:no_content) + + activity = Activity.get_by_id_with_user_actor(id) + assert activity.data["state"] == "resolved" + end end describe "GET /api/pleroma/admin/reports" do + setup do + clear_config([:instance, :admin_privileges], [:reports_manage_reports]) + end + + test "returns 403 if not privileged with :reports_manage_reports", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + conn = + conn + |> get(report_path(conn, :index)) + + assert json_response(conn, :forbidden) + end + test "returns empty response when no reports created", %{conn: conn} do response = conn @@ -292,6 +388,34 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do |> json_response_and_validate_schema(:ok) end + test "renders content correctly", %{conn: conn} do + [reporter, target_user] = insert_pair(:user) + note = insert(:note, user: target_user, data: %{"content" => "mew 1"}) + note2 = insert(:note, user: target_user, data: %{"content" => "mew 2"}) + activity = insert(:note_activity, user: target_user, note: note) + activity2 = insert(:note_activity, user: target_user, note: note2) + + {:ok, _report} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id, activity2.id] + }) + + CommonAPI.delete(activity.id, target_user) + CommonAPI.delete(activity2.id, target_user) + + response = + conn + |> get(report_path(conn, :index)) + |> json_response_and_validate_schema(:ok) + + assert [open_report] = response["reports"] + assert %{"statuses" => [s1, s2]} = open_report + assert "mew 1" in [s1["content"], s2["content"]] + assert "mew 2" in [s1["content"], s2["content"]] + end + test "returns 403 when requested by a non-admin" do user = insert(:user) token = insert(:oauth_token, user: user) @@ -313,10 +437,40 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do "error" => "Invalid credentials." } end + + test "returns reports with specified role_id", %{conn: conn} do + [reporter, target_user] = insert_pair(:user) + + %{id: rule_id} = Rule.create(%{text: "Example rule"}) + + rule_id = to_string(rule_id) + + {:ok, %{id: report_id}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "", + rule_ids: [rule_id] + }) + + {:ok, _report} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "" + }) + + response = + conn + |> get("/api/pleroma/admin/reports?rule_id=#{rule_id}") + |> json_response_and_validate_schema(:ok) + + assert %{"reports" => [%{"id" => ^report_id}]} = response + end end describe "POST /api/pleroma/admin/reports/:id/notes" do setup %{conn: conn, admin: admin} do + clear_config([:instance, :admin_privileges], [:reports_manage_reports]) + [reporter, target_user] = insert_pair(:user) activity = insert(:note_activity, user: target_user) @@ -345,6 +499,25 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do } end + test "returns 403 if not privileged with :reports_manage_reports", %{ + conn: conn, + report_id: report_id + } do + clear_config([:instance, :admin_privileges], []) + + post_conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{ + content: "this is disgusting2!" + }) + + delete_conn = delete(conn, "/api/pleroma/admin/reports/#{report_id}/notes/note.id") + + assert json_response(post_conn, :forbidden) + assert json_response(delete_conn, :forbidden) + end + test "it creates report note", %{admin_id: admin_id, report_id: report_id} do assert [note, _] = Repo.all(ReportNote) diff --git a/test/pleroma/web/admin_api/controllers/rule_controller_test.exs b/test/pleroma/web/admin_api/controllers/rule_controller_test.exs new file mode 100644 index 000000000..96b52b272 --- /dev/null +++ b/test/pleroma/web/admin_api/controllers/rule_controller_test.exs @@ -0,0 +1,82 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.RuleControllerTest do + use Pleroma.Web.ConnCase, async: true + + import Pleroma.Factory + + alias Pleroma.Rule + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} + end + + describe "GET /api/pleroma/admin/rules" do + test "sorts rules by priority", %{conn: conn} do + %{id: id1} = Rule.create(%{text: "Example rule"}) + %{id: id2} = Rule.create(%{text: "Second rule", priority: 2}) + %{id: id3} = Rule.create(%{text: "Third rule", priority: 1}) + + id1 = to_string(id1) + id2 = to_string(id2) + id3 = to_string(id3) + + response = + conn + |> get("/api/pleroma/admin/rules") + |> json_response_and_validate_schema(:ok) + + assert [%{"id" => ^id1}, %{"id" => ^id3}, %{"id" => ^id2}] = response + end + end + + describe "POST /api/pleroma/admin/rules" do + test "creates a rule", %{conn: conn} do + %{"id" => id} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/pleroma/admin/rules", %{text: "Example rule"}) + |> json_response_and_validate_schema(:ok) + + assert %{text: "Example rule"} = Rule.get(id) + end + end + + describe "PATCH /api/pleroma/admin/rules" do + test "edits a rule", %{conn: conn} do + %{id: id} = Rule.create(%{text: "Example rule"}) + + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/rules/#{id}", %{text: "There are no rules", priority: 2}) + |> json_response_and_validate_schema(:ok) + + assert %{text: "There are no rules", priority: 2} = Rule.get(id) + end + end + + describe "DELETE /api/pleroma/admin/rules" do + test "deletes a rule", %{conn: conn} do + %{id: id} = Rule.create(%{text: "Example rule"}) + + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/admin/rules/#{id}") + |> json_response_and_validate_schema(:ok) + + assert [] = + Rule.query() + |> Pleroma.Repo.all() + end + end +end diff --git a/test/pleroma/web/admin_api/controllers/status_controller_test.exs b/test/pleroma/web/admin_api/controllers/status_controller_test.exs index 8bb96ca87..8908a2812 100644 --- a/test/pleroma/web/admin_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/status_controller_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.AdminAPI.StatusControllerTest do - use Pleroma.Web.ConnCase, async: true + use Pleroma.Web.ConnCase, async: false import Pleroma.Factory @@ -26,6 +26,10 @@ defmodule Pleroma.Web.AdminAPI.StatusControllerTest do end describe "GET /api/pleroma/admin/statuses/:id" do + setup do + clear_config([:instance, :admin_privileges], [:messages_read]) + end + test "not found", %{conn: conn} do assert conn |> get("/api/pleroma/admin/statuses/not_found") @@ -50,10 +54,17 @@ defmodule Pleroma.Web.AdminAPI.StatusControllerTest do assert account["is_active"] == actor.is_active assert account["is_confirmed"] == actor.is_confirmed end + + test "denies reading activity when not privileged", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + assert conn |> get("/api/pleroma/admin/statuses/some_id") |> json_response(:forbidden) + end end describe "PUT /api/pleroma/admin/statuses/:id" do setup do + clear_config([:instance, :admin_privileges], [:messages_delete]) activity = insert(:note_activity) %{id: activity.id} @@ -122,10 +133,20 @@ defmodule Pleroma.Web.AdminAPI.StatusControllerTest do assert %{"error" => "test - Invalid value for enum."} = json_response_and_validate_schema(conn, :bad_request) end + + test "it requires privileged role :messages_delete", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + assert conn + |> put_req_header("content-type", "application/json") + |> put("/api/pleroma/admin/statuses/some_id", %{}) + |> json_response(:forbidden) + end end describe "DELETE /api/pleroma/admin/statuses/:id" do setup do + clear_config([:instance, :admin_privileges], [:messages_delete]) activity = insert(:note_activity) %{id: activity.id} @@ -149,9 +170,22 @@ defmodule Pleroma.Web.AdminAPI.StatusControllerTest do assert json_response_and_validate_schema(conn, :not_found) == %{"error" => "Not found"} end + + test "it requires privileged role :messages_delete", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + assert conn + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/admin/statuses/some_id") + |> json_response(:forbidden) + end end describe "GET /api/pleroma/admin/statuses" do + setup do + clear_config([:instance, :admin_privileges], [:messages_read]) + end + test "returns all public and unlisted statuses", %{conn: conn, admin: admin} do blocked = insert(:user) user = insert(:user) @@ -197,5 +231,13 @@ defmodule Pleroma.Web.AdminAPI.StatusControllerTest do conn = get(conn, "/api/pleroma/admin/statuses?godmode=true") assert json_response_and_validate_schema(conn, 200) |> length() == 3 end + + test "it requires privileged role :messages_read", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + conn = get(conn, "/api/pleroma/admin/statuses") + + assert json_response(conn, :forbidden) + end end end diff --git a/test/pleroma/web/admin_api/controllers/user_controller_test.exs b/test/pleroma/web/admin_api/controllers/user_controller_test.exs index 79971be06..8edfda54c 100644 --- a/test/pleroma/web/admin_api/controllers/user_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/user_controller_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.AdminAPI.UserControllerTest do - use Pleroma.Web.ConnCase + use Pleroma.Web.ConnCase, async: false use Oban.Testing, repo: Pleroma.Repo import Mock @@ -19,6 +19,11 @@ defmodule Pleroma.Web.AdminAPI.UserControllerTest do alias Pleroma.Web.Endpoint alias Pleroma.Web.MediaProxy + setup do + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + :ok + end + setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) @@ -38,6 +43,7 @@ defmodule Pleroma.Web.AdminAPI.UserControllerTest do end test "with valid `admin_token` query parameter, skips OAuth scopes check" do + clear_config([:instance, :admin_privileges], [:users_read]) clear_config([:admin_token], "password123") user = insert(:user) @@ -47,53 +53,10 @@ defmodule Pleroma.Web.AdminAPI.UserControllerTest do assert json_response_and_validate_schema(conn, 200) end - test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope", - %{admin: admin} do - user = insert(:user) - url = "/api/pleroma/admin/users/#{user.nickname}" - - good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"]) - good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"]) - good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"]) - - bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts"]) - bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"]) - bad_token3 = nil - - for good_token <- [good_token1, good_token2, good_token3] do - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, good_token) - |> get(url) - - assert json_response_and_validate_schema(conn, 200) - end - - for good_token <- [good_token1, good_token2, good_token3] do - conn = - build_conn() - |> assign(:user, nil) - |> assign(:token, good_token) - |> get(url) - - assert json_response(conn, :forbidden) - end - - for bad_token <- [bad_token1, bad_token2, bad_token3] do - conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, bad_token) - |> get(url) - - assert json_response_and_validate_schema(conn, :forbidden) - end - end - describe "DELETE /api/pleroma/admin/users" do test "single user", %{admin: admin, conn: conn} do clear_config([:instance, :federating], true) + clear_config([:instance, :admin_privileges], [:users_delete]) user = insert(:user, @@ -149,6 +112,8 @@ defmodule Pleroma.Web.AdminAPI.UserControllerTest do end test "multiple users", %{admin: admin, conn: conn} do + clear_config([:instance, :admin_privileges], [:users_delete]) + user_one = insert(:user) user_two = insert(:user) @@ -168,6 +133,17 @@ defmodule Pleroma.Web.AdminAPI.UserControllerTest do assert response -- [user_one.nickname, user_two.nickname] == [] end + + test "Needs privileged role", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + response = + conn + |> put_req_header("accept", "application/json") + |> delete("/api/pleroma/admin/users?nickname=nickname") + + assert json_response(response, :forbidden) + end end describe "/api/pleroma/admin/users" do @@ -307,7 +283,19 @@ defmodule Pleroma.Web.AdminAPI.UserControllerTest do end end - describe "/api/pleroma/admin/users/:nickname" do + describe "GET /api/pleroma/admin/users/:nickname" do + setup do + clear_config([:instance, :admin_privileges], [:users_read]) + end + + test "returns 403 if not privileged with :users_read", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + conn = get(conn, "/api/pleroma/admin/users/user.nickname") + + assert json_response(conn, :forbidden) + end + test "Show", %{conn: conn} do user = insert(:user) @@ -323,6 +311,50 @@ defmodule Pleroma.Web.AdminAPI.UserControllerTest do assert %{"error" => "Not found"} == json_response_and_validate_schema(conn, 404) end + + test "requires admin:read:accounts or broader scope", + %{admin: admin} do + user = insert(:user) + url = "/api/pleroma/admin/users/#{user.nickname}" + + good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"]) + good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"]) + good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"]) + + bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts"]) + bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"]) + bad_token3 = nil + + for good_token <- [good_token1, good_token2, good_token3] do + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, good_token) + |> get(url) + + assert json_response_and_validate_schema(conn, 200) + end + + for good_token <- [good_token1, good_token2, good_token3] do + conn = + build_conn() + |> assign(:user, nil) + |> assign(:token, good_token) + |> get(url) + + assert json_response(conn, :forbidden) + end + + for bad_token <- [bad_token1, bad_token2, bad_token3] do + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, bad_token) + |> get(url) + + assert json_response_and_validate_schema(conn, :forbidden) + end + end end describe "/api/pleroma/admin/users/follow" do @@ -378,6 +410,18 @@ defmodule Pleroma.Web.AdminAPI.UserControllerTest do end describe "GET /api/pleroma/admin/users" do + setup do + clear_config([:instance, :admin_privileges], [:users_read]) + end + + test "returns 403 if not privileged with :users_read", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + conn = get(conn, "/api/pleroma/admin/users?page=1") + + assert json_response(conn, :forbidden) + end + test "renders users array for the first page", %{conn: conn, admin: admin} do user = insert(:user, local: false, tags: ["foo", "bar"]) user2 = insert(:user, is_approved: false, registration_reason: "I'm a chill dude") @@ -810,67 +854,42 @@ defmodule Pleroma.Web.AdminAPI.UserControllerTest do end end - test "PATCH /api/pleroma/admin/users/activate", %{admin: admin, conn: conn} do - user_one = insert(:user, is_active: false) - user_two = insert(:user, is_active: false) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> patch( - "/api/pleroma/admin/users/activate", - %{nicknames: [user_one.nickname, user_two.nickname]} - ) - - response = json_response_and_validate_schema(conn, 200) - assert Enum.map(response["users"], & &1["is_active"]) == [true, true] - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} activated users: @#{user_one.nickname}, @#{user_two.nickname}" - end + test "PATCH /api/pleroma/admin/users/approve", %{admin: admin, conn: conn} do + clear_config([:instance, :admin_privileges], [:users_manage_invites]) - test "PATCH /api/pleroma/admin/users/deactivate", %{admin: admin, conn: conn} do - user_one = insert(:user, is_active: true) - user_two = insert(:user, is_active: true) + user_one = insert(:user, is_approved: false) + user_two = insert(:user, is_approved: false) conn = conn |> put_req_header("content-type", "application/json") |> patch( - "/api/pleroma/admin/users/deactivate", + "/api/pleroma/admin/users/approve", %{nicknames: [user_one.nickname, user_two.nickname]} ) response = json_response_and_validate_schema(conn, 200) - assert Enum.map(response["users"], & &1["is_active"]) == [false, false] + assert Enum.map(response["users"], & &1["is_approved"]) == [true, true] log_entry = Repo.one(ModerationLog) assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} deactivated users: @#{user_one.nickname}, @#{user_two.nickname}" + "@#{admin.nickname} approved users: @#{user_one.nickname}, @#{user_two.nickname}" end - test "PATCH /api/pleroma/admin/users/approve", %{admin: admin, conn: conn} do - user_one = insert(:user, is_approved: false) - user_two = insert(:user, is_approved: false) + test "PATCH /api/pleroma/admin/users/approve returns 403 if not privileged with :users_manage_invites", + %{conn: conn} do + clear_config([:instance, :admin_privileges], []) conn = conn |> put_req_header("content-type", "application/json") |> patch( "/api/pleroma/admin/users/approve", - %{nicknames: [user_one.nickname, user_two.nickname]} + %{nicknames: ["user_one.nickname", "user_two.nickname"]} ) - response = json_response_and_validate_schema(conn, 200) - assert Enum.map(response["users"], & &1["is_approved"]) == [true, true] - - log_entry = Repo.one(ModerationLog) - - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} approved users: @#{user_one.nickname}, @#{user_two.nickname}" + assert json_response(conn, :forbidden) end test "PATCH /api/pleroma/admin/users/suggest", %{admin: admin, conn: conn} do @@ -923,24 +942,113 @@ defmodule Pleroma.Web.AdminAPI.UserControllerTest do "@#{admin.nickname} removed suggested users: @#{user1.nickname}, @#{user2.nickname}" end - test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admin, conn: conn} do - user = insert(:user) + describe "user activation" do + test "PATCH /api/pleroma/admin/users/activate", %{admin: admin, conn: conn} do + clear_config([:instance, :admin_privileges], [:users_manage_activation_state]) - conn = - conn - |> put_req_header("content-type", "application/json") - |> patch("/api/pleroma/admin/users/#{user.nickname}/toggle_activation") + user_one = insert(:user, is_active: false) + user_two = insert(:user, is_active: false) - assert json_response_and_validate_schema(conn, 200) == - user_response( - user, - %{"is_active" => !user.is_active} - ) + conn = + conn + |> put_req_header("content-type", "application/json") + |> patch( + "/api/pleroma/admin/users/activate", + %{nicknames: [user_one.nickname, user_two.nickname]} + ) - log_entry = Repo.one(ModerationLog) + response = json_response_and_validate_schema(conn, 200) + assert Enum.map(response["users"], & &1["is_active"]) == [true, true] - assert ModerationLog.get_log_entry_message(log_entry) == - "@#{admin.nickname} deactivated users: @#{user.nickname}" + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} activated users: @#{user_one.nickname}, @#{user_two.nickname}" + end + + test "PATCH /api/pleroma/admin/users/deactivate", %{admin: admin, conn: conn} do + clear_config([:instance, :admin_privileges], [:users_manage_activation_state]) + + user_one = insert(:user, is_active: true) + user_two = insert(:user, is_active: true) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> patch( + "/api/pleroma/admin/users/deactivate", + %{nicknames: [user_one.nickname, user_two.nickname]} + ) + + response = json_response_and_validate_schema(conn, 200) + assert Enum.map(response["users"], & &1["is_active"]) == [false, false] + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deactivated users: @#{user_one.nickname}, @#{user_two.nickname}" + end + + test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admin, conn: conn} do + clear_config([:instance, :admin_privileges], [:users_manage_activation_state]) + + user = insert(:user) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/users/#{user.nickname}/toggle_activation") + + assert json_response_and_validate_schema(conn, 200) == + user_response( + user, + %{"is_active" => !user.is_active} + ) + + log_entry = Repo.one(ModerationLog) + + assert ModerationLog.get_log_entry_message(log_entry) == + "@#{admin.nickname} deactivated users: @#{user.nickname}" + end + + test "it requires privileged role :statuses_activation to activate", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> patch( + "/api/pleroma/admin/users/activate", + %{nicknames: ["user_one.nickname", "user_two.nickname"]} + ) + + assert json_response(conn, :forbidden) + end + + test "it requires privileged role :statuses_activation to deactivate", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> patch( + "/api/pleroma/admin/users/deactivate", + %{nicknames: ["user_one.nickname", "user_two.nickname"]} + ) + + assert json_response(conn, :forbidden) + end + + test "it requires privileged role :statuses_activation to toggle activation", %{conn: conn} do + clear_config([:instance, :admin_privileges], []) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/users/user.nickname/toggle_activation") + + assert json_response(conn, :forbidden) + end end defp user_response(user, attrs \\ %{}) do diff --git a/test/pleroma/web/admin_api/views/report_view_test.exs b/test/pleroma/web/admin_api/views/report_view_test.exs index 9637c2b90..1b16aca6a 100644 --- a/test/pleroma/web/admin_api/views/report_view_test.exs +++ b/test/pleroma/web/admin_api/views/report_view_test.exs @@ -7,6 +7,7 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do import Pleroma.Factory + alias Pleroma.Rule alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI.Report alias Pleroma.Web.AdminAPI.ReportView @@ -38,7 +39,8 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do statuses: [], notes: [], state: "open", - id: activity.id + id: activity.id, + rules: [] } result = @@ -76,7 +78,8 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do statuses: [StatusView.render("show.json", %{activity: activity})], state: "open", notes: [], - id: report_activity.id + id: report_activity.id, + rules: [] } result = @@ -168,4 +171,22 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do assert report2.id == rendered |> Enum.at(0) |> Map.get(:id) assert report1.id == rendered |> Enum.at(1) |> Map.get(:id) end + + test "renders included rules" do + user = insert(:user) + other_user = insert(:user) + + %{id: rule_id, text: text} = Rule.create(%{text: "Example rule"}) + + rule_id = to_string(rule_id) + + {:ok, activity} = + CommonAPI.report(user, %{ + account_id: other_user.id, + rule_ids: [rule_id] + }) + + assert %{rules: [%{id: ^rule_id, text: ^text}]} = + ReportView.render("show.json", Report.extract_report_info(activity)) + end end diff --git a/test/pleroma/web/api_spec/scopes/compiler_test.exs b/test/pleroma/web/api_spec/scopes/compiler_test.exs new file mode 100644 index 000000000..99e1d343a --- /dev/null +++ b/test/pleroma/web/api_spec/scopes/compiler_test.exs @@ -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.Web.ApiSpec.Scopes.CompilerTest do + use ExUnit.Case, async: true + + alias Pleroma.Web.ApiSpec.Scopes.Compiler + + @dummy_response %{} + + @data %{ + paths: %{ + "/mew" => %OpenApiSpex.PathItem{ + post: %OpenApiSpex.Operation{ + security: [%{"oAuth" => ["a:b:c"]}], + responses: @dummy_response + }, + get: %OpenApiSpex.Operation{security: nil, responses: @dummy_response} + }, + "/mew2" => %OpenApiSpex.PathItem{ + post: %OpenApiSpex.Operation{ + security: [%{"oAuth" => ["d:e", "f:g"]}], + responses: @dummy_response + }, + get: %OpenApiSpex.Operation{security: nil, responses: @dummy_response} + } + } + } + + describe "process_scope/1" do + test "gives all higher-level scopes" do + scopes = Compiler.process_scope("admin:read:accounts") + + assert [_, _, _] = scopes + assert "admin" in scopes + assert "admin:read" in scopes + assert "admin:read:accounts" in scopes + end + end + + describe "extract_all_scopes_from/1" do + test "extracts scopes" do + scopes = Compiler.extract_all_scopes_from(@data) + + assert [_, _, _, _, _, _, _] = scopes + assert "a" in scopes + assert "a:b" in scopes + assert "a:b:c" in scopes + assert "d" in scopes + assert "d:e" in scopes + assert "f" in scopes + assert "f:g" in scopes + end + end +end diff --git a/test/pleroma/web/common_api/activity_draft_test.exs b/test/pleroma/web/common_api/activity_draft_test.exs new file mode 100644 index 000000000..02bc6cf3b --- /dev/null +++ b/test/pleroma/web/common_api/activity_draft_test.exs @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.CommonAPI.ActivityDraftTest do + use Pleroma.DataCase + + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.CommonAPI.ActivityDraft + + import Pleroma.Factory + + test "create/2 with a quote post" do + user = insert(:user) + another_user = insert(:user) + + {:ok, direct} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) + {:ok, private} = CommonAPI.post(user, %{status: ".", visibility: "private"}) + {:ok, unlisted} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) + {:ok, local} = CommonAPI.post(user, %{status: ".", visibility: "local"}) + {:ok, public} = CommonAPI.post(user, %{status: ".", visibility: "public"}) + + {:error, _} = ActivityDraft.create(user, %{status: "nice", quote_id: direct.id}) + {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: private.id}) + {:error, _} = ActivityDraft.create(another_user, %{status: "nice", quote_id: private.id}) + {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: unlisted.id}) + {:ok, _} = ActivityDraft.create(another_user, %{status: "nice", quote_id: unlisted.id}) + {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: local.id}) + {:ok, _} = ActivityDraft.create(another_user, %{status: "nice", quote_id: local.id}) + {:ok, _} = ActivityDraft.create(user, %{status: "nice", quote_id: public.id}) + {:ok, _} = ActivityDraft.create(another_user, %{status: "nice", quote_id: public.id}) + end +end diff --git a/test/pleroma/web/common_api/utils_test.exs b/test/pleroma/web/common_api/utils_test.exs index 5b2019969..27b1da1e3 100644 --- a/test/pleroma/web/common_api/utils_test.exs +++ b/test/pleroma/web/common_api/utils_test.exs @@ -4,7 +4,6 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do alias Pleroma.Builders.UserBuilder - alias Pleroma.Object alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI.ActivityDraft alias Pleroma.Web.CommonAPI.Utils @@ -179,6 +178,10 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do code = "https://github.com/pragdave/earmark/" {result, [], []} = Utils.format_input(code, "text/markdown") assert result == ~s[<p><a href="#{code}">#{code}</a></p>] + + code = "https://github.com/~foo/bar" + {result, [], []} = Utils.format_input(code, "text/markdown") + assert result == ~s[<p><a href="#{code}">#{code}</a></p>] end test "link with local mention" do @@ -197,7 +200,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do {result, _, []} = Utils.format_input(code, "text/markdown") assert result == - ~s[<p><span class="h-card"><a class="u-url mention" data-user="#{mario.id}" href="#{mario.ap_id}" rel="ugc">@<span>mario</span></a></span> <span class="h-card"><a class="u-url mention" data-user="#{luigi.id}" href="#{luigi.ap_id}" rel="ugc">@<span>luigi</span></a></span> yo what’s up?</p>] + ~s[<p><span class="h-card"><a class="u-url mention" data-user="#{mario.id}" href="#{mario.ap_id}" rel="ugc">@<span>mario</span></a></span> <span class="h-card"><a class="u-url mention" data-user="#{luigi.id}" href="#{luigi.ap_id}" rel="ugc">@<span>luigi</span></a></span> yo what's up?</p>] end test "remote mentions" do @@ -208,7 +211,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do {result, _, []} = Utils.format_input(code, "text/markdown") assert result == - ~s[<p><span class="h-card"><a class="u-url mention" data-user="#{mario.id}" href="#{mario.ap_id}" rel="ugc">@<span>mario</span></a></span> <span class="h-card"><a class="u-url mention" data-user="#{luigi.id}" href="#{luigi.ap_id}" rel="ugc">@<span>luigi</span></a></span> yo what’s up?</p>] + ~s[<p><span class="h-card"><a class="u-url mention" data-user="#{mario.id}" href="#{mario.ap_id}" rel="ugc">@<span>mario</span></a></span> <span class="h-card"><a class="u-url mention" data-user="#{luigi.id}" href="#{luigi.ap_id}" rel="ugc">@<span>luigi</span></a></span> yo what's up?</p>] end test "raw HTML" do @@ -226,7 +229,7 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do test "blockquote" do code = ~s[> whoms't are you quoting?] {result, [], []} = Utils.format_input(code, "text/markdown") - assert result == "<blockquote><p>whoms’t are you quoting?</p></blockquote>" + assert result == "<blockquote><p>whoms't are you quoting?</p></blockquote>" end test "code" do @@ -273,22 +276,6 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do end end - describe "context_to_conversation_id" do - test "creates a mapping object" do - conversation_id = Utils.context_to_conversation_id("random context") - object = Object.get_by_ap_id("random context") - - assert conversation_id == object.id - end - - test "returns an existing mapping for an existing object" do - {:ok, object} = Object.context_mapping("random context") |> Repo.insert() - conversation_id = Utils.context_to_conversation_id("random context") - - assert conversation_id == object.id - end - end - describe "formats date to asctime" do test "when date is in ISO 8601 format" do date = DateTime.utc_now() |> DateTime.to_iso8601() @@ -517,17 +504,6 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do end end - describe "conversation_id_to_context/1" do - test "returns id" do - object = insert(:note) - assert Utils.conversation_id_to_context(object.id) == object.data["id"] - end - - test "returns error if object not found" do - assert Utils.conversation_id_to_context("123") == {:error, "No such conversation"} - end - end - describe "maybe_notify_mentioned_recipients/2" do test "returns recipients when activity is not `Create`" do activity = insert(:like_activity) @@ -610,41 +586,61 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do end end - describe "attachments_from_ids_descs/2" do + describe "attachments_from_ids_descs/3" do test "returns [] when attachment ids is empty" do - assert Utils.attachments_from_ids_descs([], "{}") == [] + assert Utils.attachments_from_ids_descs([], "{}", nil) == [] end test "returns list attachments with desc" do - object = insert(:note) + user = insert(:user) + object = insert(:attachment, %{user: user}) desc = Jason.encode!(%{object.id => "test-desc"}) - assert Utils.attachments_from_ids_descs(["#{object.id}", "34"], desc) == [ + assert Utils.attachments_from_ids_descs(["#{object.id}", "34"], desc, user) == [ Map.merge(object.data, %{"name" => "test-desc"}) ] end end - describe "attachments_from_ids/1" do + describe "attachments_from_ids/2" do test "returns attachments with descs" do - object = insert(:note) + user = insert(:user) + object = insert(:attachment, %{user: user}) desc = Jason.encode!(%{object.id => "test-desc"}) - assert Utils.attachments_from_ids(%{ - media_ids: ["#{object.id}"], - descriptions: desc - }) == [ + assert Utils.attachments_from_ids( + %{ + media_ids: ["#{object.id}"], + descriptions: desc + }, + user + ) == [ Map.merge(object.data, %{"name" => "test-desc"}) ] end test "returns attachments without descs" do - object = insert(:note) - assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}) == [object.data] + user = insert(:user) + object = insert(:attachment, %{user: user}) + assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}, user) == [object.data] end test "returns [] when not pass media_ids" do - assert Utils.attachments_from_ids(%{}) == [] + assert Utils.attachments_from_ids(%{}, nil) == [] + end + + test "returns [] when media_ids not belong to current user" do + user = insert(:user) + user2 = insert(:user) + + object = insert(:attachment, %{user: user}) + + assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}, user2) == [] + end + + test "checks that the object is of upload type" do + object = insert(:note) + assert Utils.attachments_from_ids(%{media_ids: ["#{object.id}"]}, nil) == [] end end diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index b502aaa03..58cd1fd42 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -4,7 +4,7 @@ defmodule Pleroma.Web.CommonAPITest do use Oban.Testing, repo: Pleroma.Repo - use Pleroma.DataCase + use Pleroma.DataCase, async: false alias Pleroma.Activity alias Pleroma.Chat @@ -12,6 +12,8 @@ defmodule Pleroma.Web.CommonAPITest do alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo + alias Pleroma.Rule + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Transmogrifier @@ -20,17 +22,32 @@ defmodule Pleroma.Web.CommonAPITest do alias Pleroma.Web.CommonAPI alias Pleroma.Workers.PollWorker - import Pleroma.Factory - import Mock import Ecto.Query, only: [from: 2] + import Mock + import Mox + import Pleroma.Factory + require Pleroma.Activity.Queries require Pleroma.Constants + defp get_announces_of_object(%{data: %{"id" => id}} = _object) do + Pleroma.Activity.Queries.by_type("Announce") + |> Pleroma.Activity.Queries.by_object_id(id) + |> Pleroma.Repo.all() + end + setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) :ok end + setup do + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + + :ok + end + setup do: clear_config([:instance, :safe_dm_mentions]) setup do: clear_config([:instance, :limit]) setup do: clear_config([:instance, :max_pinned_statuses]) @@ -61,9 +78,11 @@ defmodule Pleroma.Web.CommonAPITest do describe "blocking" do setup do blocker = insert(:user) - blocked = insert(:user) - User.follow(blocker, blocked) - User.follow(blocked, blocker) + blocked = insert(:user, local: false) + CommonAPI.follow(blocker, blocked) + CommonAPI.follow(blocked, blocker) + CommonAPI.accept_follow_request(blocker, blocked) + CommonAPI.accept_follow_request(blocked, blocked) %{blocker: blocker, blocked: blocked} end @@ -72,6 +91,9 @@ defmodule Pleroma.Web.CommonAPITest do with_mock Pleroma.Web.Federator, publish: fn _ -> nil end do + assert User.get_follow_state(blocker, blocked) == :follow_accept + refute is_nil(Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(blocker, blocked)) + assert {:ok, block} = CommonAPI.block(blocker, blocked) assert block.local @@ -79,6 +101,11 @@ defmodule Pleroma.Web.CommonAPITest do refute User.following?(blocker, blocked) refute User.following?(blocked, blocker) + refute User.get_follow_state(blocker, blocked) + + assert %{data: %{"state" => "reject"}} = + Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(blocker, blocked) + assert called(Pleroma.Web.Federator.publish(block)) end end @@ -269,6 +296,24 @@ defmodule Pleroma.Web.CommonAPITest do assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} == CommonAPI.post_chat_message(author, recipient, "GNO/Linux") end + + test "it reject messages with attachments not belonging to user" do + author = insert(:user) + not_author = insert(:user) + recipient = author + + attachment = insert(:attachment, %{user: not_author}) + + {:error, message} = + CommonAPI.post_chat_message( + author, + recipient, + "123", + media_id: attachment.id + ) + + assert message == :forbidden + end end describe "unblocking" do @@ -321,7 +366,7 @@ defmodule Pleroma.Web.CommonAPITest do refute Activity.get_by_id(post.id) end - test "it does not allow a user to delete their posts" do + test "it does not allow a user to delete posts from another user" do user = insert(:user) other_user = insert(:user) @@ -331,7 +376,8 @@ defmodule Pleroma.Web.CommonAPITest do assert Activity.get_by_id(post.id) end - test "it allows moderators to delete other user's posts" do + test "it allows privileged users to delete other user's posts" do + clear_config([:instance, :moderator_privileges], [:messages_delete]) user = insert(:user) moderator = insert(:user, is_moderator: true) @@ -343,19 +389,20 @@ defmodule Pleroma.Web.CommonAPITest do refute Activity.get_by_id(post.id) end - test "it allows admins to delete other user's posts" do + test "it doesn't allow unprivileged mods or admins to delete other user's posts" do + clear_config([:instance, :admin_privileges], []) + clear_config([:instance, :moderator_privileges], []) user = insert(:user) - moderator = insert(:user, is_admin: true) + moderator = insert(:user, is_moderator: true, is_admin: true) {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) - assert {:ok, delete} = CommonAPI.delete(post.id, moderator) - assert delete.local - - refute Activity.get_by_id(post.id) + assert {:error, "Could not delete"} = CommonAPI.delete(post.id, moderator) + assert Activity.get_by_id(post.id) end - test "superusers deleting non-local posts won't federate the delete" do + test "privileged users deleting non-local posts won't federate the delete" do + clear_config([:instance, :admin_privileges], [:messages_delete]) # This is the user of the ingested activity _user = insert(:user, @@ -364,7 +411,7 @@ defmodule Pleroma.Web.CommonAPITest do last_refreshed_at: NaiveDateTime.utc_now() ) - moderator = insert(:user, is_admin: true) + admin = insert(:user, is_admin: true) data = File.read!("test/fixtures/mastodon-post-activity.json") @@ -374,13 +421,27 @@ defmodule Pleroma.Web.CommonAPITest do with_mock Pleroma.Web.Federator, publish: fn _ -> nil end do - assert {:ok, delete} = CommonAPI.delete(post.id, moderator) + assert {:ok, delete} = CommonAPI.delete(post.id, admin) assert delete.local refute called(Pleroma.Web.Federator.publish(:_)) end refute Activity.get_by_id(post.id) end + + test "it allows privileged users to delete banned user's posts" do + clear_config([:instance, :moderator_privileges], [:messages_delete]) + user = insert(:user) + moderator = insert(:user, is_moderator: true) + + {:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"}) + User.set_activation(user, false) + + assert {:ok, delete} = CommonAPI.delete(post.id, moderator) + assert delete.local + + refute Activity.get_by_id(post.id) + end end test "favoriting race condition" do @@ -446,7 +507,7 @@ defmodule Pleroma.Web.CommonAPITest do {:ok, convo_reply} = CommonAPI.post(user, %{status: ".", in_reply_to_conversation_id: participation.id}) - assert Visibility.is_direct?(convo_reply) + assert Visibility.direct?(convo_reply) assert activity.data["context"] == convo_reply.data["context"] end @@ -506,6 +567,36 @@ defmodule Pleroma.Web.CommonAPITest do assert Object.tags(object) == ["2hu"] end + test "zwnj is treated as word character" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "#ساٴينس"}) + + object = Object.normalize(activity, fetch: false) + + assert Object.tags(object) == ["ساٴينس"] + end + + test "allows lang attribute" do + user = insert(:user) + text = ~s{<span lang="en">something</span><p lang="diaetuitech_rpyhpgc">random</p>} + + {:ok, activity} = CommonAPI.post(user, %{status: text, content_type: "text/html"}) + + object = Object.normalize(activity, fetch: false) + + assert object.data["content"] == text + end + + test "double dot in link is allowed" do + user = insert(:user) + text = "https://example.to/something..mp3" + {:ok, activity} = CommonAPI.post(user, %{status: text}) + + object = Object.normalize(activity, fetch: false) + + assert object.data["content"] == "<a href=\"#{text}\" rel=\"ugc\">#{text}</a>" + end + test "it adds emoji in the object" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{status: ":firefox:"}) @@ -586,7 +677,7 @@ defmodule Pleroma.Web.CommonAPITest do object = Object.normalize(activity, fetch: false) assert object.data["content"] == "<p><b>2hu</b></p>alert('xss')" - assert object.data["source"] == post + assert object.data["source"]["content"] == post end test "it filters out obviously bad tags when accepting a post as Markdown" do @@ -603,7 +694,7 @@ defmodule Pleroma.Web.CommonAPITest do object = Object.normalize(activity, fetch: false) assert object.data["content"] == "<p><b>2hu</b></p>" - assert object.data["source"] == post + assert object.data["source"]["content"] == post end test "it does not allow replies to direct messages that are not direct messages themselves" do @@ -722,6 +813,65 @@ defmodule Pleroma.Web.CommonAPITest do scheduled_at: expires_at ) end + + test "it allows quote posting" do + user = insert(:user) + + {:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"}) + {:ok, quote_post} = CommonAPI.post(user, %{status: "nice post", quote_id: quoted.id}) + + quoted = Object.normalize(quoted) + quote_post = Object.normalize(quote_post) + + assert quote_post.data["quoteUrl"] == quoted.data["id"] + + # The OP is not mentioned + refute quoted.data["actor"] in quote_post.data["to"] + end + + test "quote posting with explicit addressing doesn't mention the OP" do + user = insert(:user) + + {:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"}) + + {:ok, quote_post} = + CommonAPI.post(user, %{status: "nice post", quote_id: quoted.id, to: []}) + + assert Object.normalize(quote_post).data["to"] == [Pleroma.Constants.as_public()] + end + + test "quote posting visibility" do + user = insert(:user) + another_user = insert(:user) + + {:ok, direct} = CommonAPI.post(user, %{status: ".", visibility: "direct"}) + {:ok, private} = CommonAPI.post(user, %{status: ".", visibility: "private"}) + {:ok, unlisted} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"}) + {:ok, local} = CommonAPI.post(user, %{status: ".", visibility: "local"}) + {:ok, public} = CommonAPI.post(user, %{status: ".", visibility: "public"}) + + {:error, _} = CommonAPI.post(user, %{status: "nice", quote_id: direct.id}) + {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: private.id}) + {:error, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: private.id}) + {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: unlisted.id}) + {:ok, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: unlisted.id}) + {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: local.id}) + {:ok, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: local.id}) + {:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: public.id}) + {:ok, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: public.id}) + end + + test "it properly mentions punycode domain" do + user = insert(:user) + + _mentioned_user = + insert(:user, ap_id: "https://xn--i2raa.com/users/yyy", nickname: "yyy@xn--i2raa.com") + + {:ok, activity} = + CommonAPI.post(user, %{status: "hey @yyy@xn--i2raa.com", content_type: "text/markdown"}) + + assert "https://xn--i2raa.com/users/yyy" in Object.normalize(activity).data["to"] + end end describe "reactions" do @@ -775,7 +925,7 @@ defmodule Pleroma.Web.CommonAPITest do {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"}) {:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, user) - assert Visibility.is_public?(announce_activity) + assert Visibility.public?(announce_activity) end test "can't repeat a repeat" do @@ -797,7 +947,7 @@ defmodule Pleroma.Web.CommonAPITest do {:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, user, %{visibility: "private"}) - assert Visibility.is_private?(announce_activity) + assert Visibility.private?(announce_activity) refute Visibility.visible_for_user?(announce_activity, nil) end @@ -810,7 +960,7 @@ defmodule Pleroma.Web.CommonAPITest do {:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, author) - assert Visibility.is_private?(announce_activity) + assert Visibility.private?(announce_activity) refute Visibility.visible_for_user?(announce_activity, nil) assert Visibility.visible_for_user?(activity, follower) @@ -1090,10 +1240,11 @@ defmodule Pleroma.Web.CommonAPITest do target_user = insert(:user) {:ok, activity} = CommonAPI.post(target_user, %{status: "foobar"}) + activity = Activity.normalize(activity) reporter_ap_id = reporter.ap_id target_ap_id = target_user.ap_id - activity_ap_id = activity.data["id"] + reported_object_ap_id = activity.object.data["id"] comment = "foobar" report_data = %{ @@ -1104,7 +1255,7 @@ defmodule Pleroma.Web.CommonAPITest do note_obj = %{ "type" => "Note", - "id" => activity_ap_id, + "id" => reported_object_ap_id, "content" => "foobar", "published" => activity.object.data["published"], "actor" => AccountView.render("show.json", %{user: target_user}) @@ -1126,6 +1277,7 @@ defmodule Pleroma.Web.CommonAPITest do test "updates report state" do [reporter, target_user] = insert_pair(:user) activity = insert(:note_activity, user: target_user) + object = Object.normalize(activity) {:ok, %Activity{id: report_id}} = CommonAPI.report(reporter, %{ @@ -1138,10 +1290,36 @@ defmodule Pleroma.Web.CommonAPITest do assert report.data["state"] == "resolved" - [reported_user, activity_id] = report.data["object"] + [reported_user, object_id] = report.data["object"] + + assert reported_user == target_user.ap_id + assert object_id == object.data["id"] + end + + test "updates report state, don't strip when report_strip_status is false" do + clear_config([:instance, :report_strip_status], false) + + [reporter, target_user] = insert_pair(:user) + activity = insert(:note_activity, user: target_user) + + {:ok, %Activity{id: report_id, data: report_data}} = + CommonAPI.report(reporter, %{ + account_id: target_user.id, + comment: "I feel offended", + status_ids: [activity.id] + }) + + {:ok, report} = CommonAPI.update_report_state(report_id, "resolved") + + assert report.data["state"] == "resolved" + + [reported_user, reported_activity] = report.data["object"] assert reported_user == target_user.ap_id - assert activity_id == activity.data["id"] + assert is_map(reported_activity) + + assert reported_activity["content"] == + report_data["object"] |> Enum.at(1) |> Map.get("content") end test "does not update report state when state is unsupported" do @@ -1186,6 +1364,33 @@ defmodule Pleroma.Web.CommonAPITest do assert first_report.data["state"] == "resolved" assert second_report.data["state"] == "resolved" end + + test "creates a report with provided rules" do + reporter = insert(:user) + target_user = insert(:user) + + %{id: rule_id} = Rule.create(%{text: "There are no rules"}) + + reporter_ap_id = reporter.ap_id + target_ap_id = target_user.ap_id + + report_data = %{ + account_id: target_user.id, + rule_ids: [rule_id] + } + + assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data) + + assert %Activity{ + actor: ^reporter_ap_id, + data: %{ + "type" => "Flag", + "object" => [^target_ap_id], + "state" => "open", + "rules" => [^rule_id] + } + } = flag_activity + end end describe "reblog muting" do @@ -1269,7 +1474,7 @@ defmodule Pleroma.Web.CommonAPITest do test "cancels a pending follow for a remote user" do follower = insert(:user) - followed = insert(:user, is_locked: true, local: false, ap_enabled: true) + followed = insert(:user, is_locked: true, local: false) assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} = CommonAPI.follow(follower, followed) @@ -1427,7 +1632,7 @@ defmodule Pleroma.Web.CommonAPITest do with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"}) - assert Visibility.is_local_public?(activity) + assert Visibility.local_public?(activity) assert_not_called(Pleroma.Web.Federator.publish(activity)) end end @@ -1442,7 +1647,7 @@ defmodule Pleroma.Web.CommonAPITest do assert {:ok, %Activity{data: %{"deleted_activity_id" => ^activity_id}} = activity} = CommonAPI.delete(activity_id, user) - assert Visibility.is_local_public?(activity) + assert Visibility.local_public?(activity) assert_not_called(Pleroma.Web.Federator.publish(activity)) end end @@ -1458,7 +1663,7 @@ defmodule Pleroma.Web.CommonAPITest do assert {:ok, %Activity{data: %{"type" => "Announce"}} = activity} = CommonAPI.repeat(activity_id, user) - assert Visibility.is_local_public?(activity) + assert Visibility.local_public?(activity) refute called(Pleroma.Web.Federator.publish(activity)) end end @@ -1476,7 +1681,7 @@ defmodule Pleroma.Web.CommonAPITest do assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} = CommonAPI.unrepeat(activity_id, user) - assert Visibility.is_local_public?(activity) + assert Visibility.local_public?(activity) refute called(Pleroma.Web.Federator.publish(activity)) end end @@ -1491,7 +1696,7 @@ defmodule Pleroma.Web.CommonAPITest do assert {:ok, %Activity{data: %{"type" => "Like"}} = activity} = CommonAPI.favorite(user, activity.id) - assert Visibility.is_local_public?(activity) + assert Visibility.local_public?(activity) refute called(Pleroma.Web.Federator.publish(activity)) end end @@ -1506,7 +1711,7 @@ defmodule Pleroma.Web.CommonAPITest do with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do assert {:ok, activity} = CommonAPI.unfavorite(activity.id, user) - assert Visibility.is_local_public?(activity) + assert Visibility.local_public?(activity) refute called(Pleroma.Web.Federator.publish(activity)) end end @@ -1520,7 +1725,7 @@ defmodule Pleroma.Web.CommonAPITest do assert {:ok, %Activity{data: %{"type" => "EmojiReact"}} = activity} = CommonAPI.react_with_emoji(activity.id, user, "👍") - assert Visibility.is_local_public?(activity) + assert Visibility.local_public?(activity) refute called(Pleroma.Web.Federator.publish(activity)) end end @@ -1536,9 +1741,183 @@ defmodule Pleroma.Web.CommonAPITest do assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} = CommonAPI.unreact_with_emoji(activity.id, user, "👍") - assert Visibility.is_local_public?(activity) + assert Visibility.local_public?(activity) refute called(Pleroma.Web.Federator.publish(activity)) end end end + + describe "update/3" do + test "updates a post" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1"}) + + {:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2"}) + + updated_object = Object.normalize(updated) + assert updated_object.data["content"] == "updated 2" + assert Map.get(updated_object.data, "summary", "") == "" + assert Map.has_key?(updated_object.data, "updated") + end + + test "does not change visibility" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1", visibility: "private"}) + + {:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2"}) + + updated_object = Object.normalize(updated) + assert updated_object.data["content"] == "updated 2" + assert Map.get(updated_object.data, "summary", "") == "" + assert Visibility.get_visibility(updated_object) == "private" + assert Visibility.get_visibility(updated) == "private" + end + + test "updates a post with emoji" do + [{emoji1, _}, {emoji2, _} | _] = Pleroma.Emoji.get_all() + + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1 :#{emoji1}:"}) + + {:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2 :#{emoji2}:"}) + + updated_object = Object.normalize(updated) + assert updated_object.data["content"] == "updated 2 :#{emoji2}:" + assert %{^emoji2 => _} = updated_object.data["emoji"] + end + + test "updates a post with emoji and federate properly" do + [{emoji1, _}, {emoji2, _} | _] = Pleroma.Emoji.get_all() + + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1 :#{emoji1}:"}) + + clear_config([:instance, :federating], true) + + with_mock Pleroma.Web.Federator, + publish: fn _p -> nil end do + {:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2 :#{emoji2}:"}) + + assert updated.data["object"]["content"] == "updated 2 :#{emoji2}:" + assert %{^emoji2 => _} = updated.data["object"]["emoji"] + + assert called(Pleroma.Web.Federator.publish(updated)) + end + end + + test "editing a post that copied a remote title with remote emoji should keep that emoji" do + remote_emoji_uri = "https://remote.org/emoji.png" + + note = + insert( + :note, + data: %{ + "summary" => ":remoteemoji:", + "emoji" => %{ + "remoteemoji" => remote_emoji_uri + }, + "tag" => [ + %{ + "type" => "Emoji", + "name" => "remoteemoji", + "icon" => %{"url" => remote_emoji_uri} + } + ] + } + ) + + note_activity = insert(:note_activity, note: note) + + user = insert(:user) + + {:ok, reply} = + CommonAPI.post(user, %{ + status: "reply", + spoiler_text: ":remoteemoji:", + in_reply_to_id: note_activity.id + }) + + assert reply.object.data["emoji"]["remoteemoji"] == remote_emoji_uri + + {:ok, edit} = + CommonAPI.update(user, reply, %{status: "reply mew mew", spoiler_text: ":remoteemoji:"}) + + edited_note = Pleroma.Object.normalize(edit) + + assert edited_note.data["emoji"]["remoteemoji"] == remote_emoji_uri + end + + test "respects MRF" do + user = insert(:user) + + clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy]) + clear_config([:mrf_keyword, :replace], [{"updated", "mewmew"}]) + + {:ok, activity} = CommonAPI.post(user, %{status: "foo1", spoiler_text: "updated 1"}) + assert Object.normalize(activity).data["summary"] == "mewmew 1" + + {:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2"}) + + updated_object = Object.normalize(updated) + assert updated_object.data["content"] == "mewmew 2" + assert Map.get(updated_object.data, "summary", "") == "" + assert Map.has_key?(updated_object.data, "updated") + end + end + + describe "Group actors" do + setup do + poster = insert(:user) + group = insert(:user, actor_type: "Group") + other_group = insert(:user, actor_type: "Group") + %{poster: poster, group: group, other_group: other_group} + end + + test "it boosts public posts", %{poster: poster, group: group} do + {:ok, post} = CommonAPI.post(poster, %{status: "hey @#{group.nickname}"}) + + announces = get_announces_of_object(post.object) + assert [_] = announces + end + + test "it does not boost private posts", %{poster: poster, group: group} do + {:ok, private_post} = + CommonAPI.post(poster, %{status: "hey @#{group.nickname}", visibility: "private"}) + + assert [] = get_announces_of_object(private_post.object) + end + + test "remote groups do not boost any posts", %{poster: poster} do + remote_group = + insert(:user, actor_type: "Group", local: false, nickname: "remote@example.com") + + {:ok, post} = CommonAPI.post(poster, %{status: "hey @#{User.full_nickname(remote_group)}"}) + assert remote_group.ap_id in post.data["to"] + + announces = get_announces_of_object(post.object) + assert [] = announces + end + + test "multiple groups mentioned", %{poster: poster, group: group, other_group: other_group} do + {:ok, post} = + CommonAPI.post(poster, %{status: "hey @#{group.nickname} @#{other_group.nickname}"}) + + announces = get_announces_of_object(post.object) + assert [_, _] = announces + end + + test "it does not boost if group is blocking poster", %{poster: poster, group: group} do + {:ok, _} = CommonAPI.block(group, poster) + {:ok, post} = CommonAPI.post(poster, %{status: "hey @#{group.nickname}"}) + + announces = get_announces_of_object(post.object) + assert [] = announces + end + end end diff --git a/test/pleroma/web/endpoint/metrics_exporter_test.exs b/test/pleroma/web/endpoint/metrics_exporter_test.exs deleted file mode 100644 index ad236d4cb..000000000 --- a/test/pleroma/web/endpoint/metrics_exporter_test.exs +++ /dev/null @@ -1,69 +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.Web.Endpoint.MetricsExporterTest do - # Modifies AppEnv, has to stay synchronous - use Pleroma.Web.ConnCase - - alias Pleroma.Web.Endpoint.MetricsExporter - - defp config do - Application.get_env(:prometheus, MetricsExporter) - end - - describe "with default config" do - test "does NOT expose app metrics", %{conn: conn} do - conn - |> get(config()[:path]) - |> json_response(404) - end - end - - describe "when enabled" do - setup do - initial_config = config() - on_exit(fn -> Application.put_env(:prometheus, MetricsExporter, initial_config) end) - - Application.put_env( - :prometheus, - MetricsExporter, - Keyword.put(initial_config, :enabled, true) - ) - end - - test "serves app metrics", %{conn: conn} do - conn = get(conn, config()[:path]) - assert response = response(conn, 200) - - for metric <- [ - "http_requests_total", - "http_request_duration_microseconds", - "phoenix_controller_call_duration", - "telemetry_scrape_duration", - "erlang_vm_memory_atom_bytes_total" - ] do - assert response =~ ~r/#{metric}/ - end - end - - test "when IP whitelist configured, " <> - "serves app metrics only if client IP is whitelisted", - %{conn: conn} do - Application.put_env( - :prometheus, - MetricsExporter, - Keyword.put(config(), :ip_whitelist, ["127.127.127.127", {1, 1, 1, 1}, '255.255.255.255']) - ) - - conn - |> get(config()[:path]) - |> json_response(404) - - conn - |> Map.put(:remote_ip, {127, 127, 127, 127}) - |> get(config()[:path]) - |> response(200) - end - end -end diff --git a/test/pleroma/web/fallback_test.exs b/test/pleroma/web/fallback_test.exs index 6d11d4f37..ed34d6490 100644 --- a/test/pleroma/web/fallback_test.exs +++ b/test/pleroma/web/fallback_test.exs @@ -6,20 +6,6 @@ defmodule Pleroma.Web.FallbackTest do use Pleroma.Web.ConnCase import Pleroma.Factory - describe "neither preloaded data nor metadata attached to" do - test "GET /registration/:token", %{conn: conn} do - response = get(conn, "/registration/foo") - - assert html_response(response, 200) =~ "<!--server-generated-meta-->" - end - - test "GET /*path", %{conn: conn} do - assert conn - |> get("/foo") - |> html_response(200) =~ "<!--server-generated-meta-->" - end - end - test "GET /*path adds a title", %{conn: conn} do clear_config([:instance, :name], "a cool title") @@ -29,21 +15,28 @@ defmodule Pleroma.Web.FallbackTest do end describe "preloaded data and metadata attached to" do - test "GET /:maybe_nickname_or_id", %{conn: conn} do + test "GET /:maybe_nickname_or_id with existing user", %{conn: conn} do clear_config([:instance, :name], "a cool title") - user = insert(:user) - user_missing = get(conn, "/foo") - user_present = get(conn, "/#{user.nickname}") - assert html_response(user_missing, 200) =~ "<!--server-generated-meta-->" - refute html_response(user_present, 200) =~ "<!--server-generated-meta-->" - assert html_response(user_present, 200) =~ "initial-results" - assert html_response(user_present, 200) =~ "<title>a cool title</title>" + resp = get(conn, "/#{user.nickname}") + + assert html_response(resp, 200) =~ "<title>a cool title</title>" + refute html_response(resp, 200) =~ "<!--server-generated-meta-->" + assert html_response(resp, 200) =~ "initial-results" + end + + test "GET /:maybe_nickname_or_id with missing user", %{conn: conn} do + clear_config([:instance, :name], "a cool title") + + resp = get(conn, "/foo") + + assert html_response(resp, 200) =~ "<title>a cool title</title>" + refute html_response(resp, 200) =~ "initial-results" end test "GET /*path", %{conn: conn} do - assert conn + refute conn |> get("/foo") |> html_response(200) =~ "<!--server-generated-meta-->" @@ -65,10 +58,12 @@ defmodule Pleroma.Web.FallbackTest do end test "GET /main/all", %{conn: conn} do + clear_config([:instance, :name], "a cool title") public_page = get(conn, "/main/all") refute html_response(public_page, 200) =~ "<!--server-generated-meta-->" assert html_response(public_page, 200) =~ "initial-results" + assert html_response(public_page, 200) =~ "<title>a cool title</title>" end end diff --git a/test/pleroma/web/federator_test.exs b/test/pleroma/web/federator_test.exs index 5120bf57c..4a398f239 100644 --- a/test/pleroma/web/federator_test.exs +++ b/test/pleroma/web/federator_test.exs @@ -40,6 +40,44 @@ defmodule Pleroma.Web.FederatorTest do %{activity: activity, relay_mock: relay_mock} end + test "to shared inbox when multiple actors from same instance are recipients" do + user = insert(:user) + + shared_inbox = "https://domain.com/inbox" + + follower_one = + insert(:user, %{ + local: false, + nickname: "nick1@domain.com", + ap_id: "https://domain.com/users/nick1", + inbox: "https://domain.com/users/nick1/inbox", + shared_inbox: shared_inbox + }) + + follower_two = + insert(:user, %{ + local: false, + nickname: "nick2@domain.com", + ap_id: "https://domain.com/users/nick2", + inbox: "https://domain.com/users/nick2/inbox", + shared_inbox: shared_inbox + }) + + {:ok, _, _} = Pleroma.User.follow(follower_one, user) + {:ok, _, _} = Pleroma.User.follow(follower_two, user) + + {:ok, _activity} = CommonAPI.post(user, %{status: "Happy Friday everyone!"}) + + ObanHelpers.perform(all_enqueued(worker: PublisherWorker)) + + inboxes = + all_enqueued(worker: PublisherWorker) + |> Enum.filter(&(get_in(&1, [Access.key(:args), Access.key("op")]) == "publish_one")) + |> Enum.map(&get_in(&1, [Access.key(:args), Access.key("params"), Access.key("inbox")])) + + assert [shared_inbox] == inboxes + end + test "with relays active, it publishes to the relay", %{ activity: activity, relay_mock: relay_mock @@ -78,16 +116,14 @@ defmodule Pleroma.Web.FederatorTest do local: false, nickname: "nick1@domain.com", ap_id: "https://domain.com/users/nick1", - inbox: inbox1, - ap_enabled: true + inbox: inbox1 }) insert(:user, %{ local: false, nickname: "nick2@domain2.com", ap_id: "https://domain2.com/users/nick2", - inbox: inbox2, - ap_enabled: true + inbox: inbox2 }) dt = NaiveDateTime.utc_now() @@ -133,7 +169,7 @@ defmodule Pleroma.Web.FederatorTest do assert {:ok, _activity} = ObanHelpers.perform(job) assert {:ok, job} = Federator.incoming_ap_doc(params) - assert {:error, :already_present} = ObanHelpers.perform(job) + assert {:cancel, :already_present} = ObanHelpers.perform(job) end test "rejects incoming AP docs with incorrect origin" do @@ -153,7 +189,7 @@ defmodule Pleroma.Web.FederatorTest do } assert {:ok, job} = Federator.incoming_ap_doc(params) - assert {:error, :origin_containment_failed} = ObanHelpers.perform(job) + assert {:cancel, :origin_containment_failed} = ObanHelpers.perform(job) end test "it does not crash if MRF rejects the post" do @@ -169,7 +205,7 @@ defmodule Pleroma.Web.FederatorTest do |> Jason.decode!() assert {:ok, job} = Federator.incoming_ap_doc(params) - assert {:error, _} = ObanHelpers.perform(job) + assert {:cancel, _} = ObanHelpers.perform(job) end end end diff --git a/test/pleroma/web/feed/tag_controller_test.exs b/test/pleroma/web/feed/tag_controller_test.exs index 1bd2b157c..58ab8f137 100644 --- a/test/pleroma/web/feed/tag_controller_test.exs +++ b/test/pleroma/web/feed/tag_controller_test.exs @@ -63,7 +63,6 @@ defmodule Pleroma.Web.Feed.TagControllerTest do ] assert xpath(xml, ~x"//feed/entry/author/name/text()"ls) == [user.nickname, user.nickname] - assert xpath(xml, ~x"//feed/entry/author/id/text()"ls) == [user.ap_id, user.ap_id] conn = conn @@ -138,8 +137,8 @@ defmodule Pleroma.Web.Feed.TagControllerTest do ] assert xpath(xml, ~x"//channel/item/pubDate/text()"sl) == [ - FeedView.pub_date(activity2.data["published"]), - FeedView.pub_date(activity1.data["published"]) + FeedView.to_rfc2822(activity2.data["published"]), + FeedView.to_rfc2822(activity1.data["published"]) ] assert xpath(xml, ~x"//channel/item/enclosure/@url"sl) == [ diff --git a/test/pleroma/web/feed/user_controller_test.exs b/test/pleroma/web/feed/user_controller_test.exs index 38cde3315..d3c4108de 100644 --- a/test/pleroma/web/feed/user_controller_test.exs +++ b/test/pleroma/web/feed/user_controller_test.exs @@ -57,9 +57,23 @@ defmodule Pleroma.Web.Feed.UserControllerTest do ) note_activity2 = insert(:note_activity, note: note2) + + note3 = + insert(:note, + user: user, + data: %{ + "content" => "This note tests whether HTML entities are truncated properly", + "summary" => "Won't, didn't fail", + "inReplyTo" => note_activity2.id + } + ) + + _note_activity3 = insert(:note_activity, note: note3) object = Object.normalize(note_activity, fetch: false) - [user: user, object: object, max_id: note_activity2.id] + encoded_title = FeedView.activity_title(note3.data) + + [user: user, object: object, max_id: note_activity2.id, encoded_title: encoded_title] end test "gets an atom feed", %{conn: conn, user: user, object: object, max_id: max_id} do @@ -74,7 +88,7 @@ defmodule Pleroma.Web.Feed.UserControllerTest do |> SweetXml.parse() |> SweetXml.xpath(~x"//entry/title/text()"l) - assert activity_titles == ['42 & Thi...', 'This & t...'] + assert activity_titles == ['Won\'t, didn\'...', '2hu', '2hu & as'] assert resp =~ FeedView.escape(object.data["content"]) assert resp =~ FeedView.escape(object.data["summary"]) assert resp =~ FeedView.escape(object.data["context"]) @@ -90,7 +104,7 @@ defmodule Pleroma.Web.Feed.UserControllerTest do |> SweetXml.parse() |> SweetXml.xpath(~x"//entry/title/text()"l) - assert activity_titles == ['This & t...'] + assert activity_titles == ['2hu & as'] end test "gets a rss feed", %{conn: conn, user: user, object: object, max_id: max_id} do @@ -105,7 +119,7 @@ defmodule Pleroma.Web.Feed.UserControllerTest do |> SweetXml.parse() |> SweetXml.xpath(~x"//item/title/text()"l) - assert activity_titles == ['42 & Thi...', 'This & t...'] + assert activity_titles == ['Won\'t, didn\'...', '2hu', '2hu & as'] assert resp =~ FeedView.escape(object.data["content"]) assert resp =~ FeedView.escape(object.data["summary"]) assert resp =~ FeedView.escape(object.data["context"]) @@ -121,7 +135,7 @@ defmodule Pleroma.Web.Feed.UserControllerTest do |> SweetXml.parse() |> SweetXml.xpath(~x"//item/title/text()"l) - assert activity_titles == ['This & t...'] + assert activity_titles == ['2hu & as'] end test "returns 404 for a missing feed", %{conn: conn} do @@ -176,6 +190,30 @@ defmodule Pleroma.Web.Feed.UserControllerTest do |> get("/users/#{user.nickname}/feed.rss") |> response(200) end + + test "does not mangle HTML entities midway", %{ + conn: conn, + user: user, + object: object, + encoded_title: encoded_title + } do + resp = + conn + |> put_req_header("accept", "application/atom+xml") + |> get(user_feed_path(conn, :feed, user.nickname)) + |> response(200) + + activity_titles = + resp + |> SweetXml.parse() + |> SweetXml.xpath(~x"//entry/title/text()"l) + + assert activity_titles == ['Won\'t, didn\'...', '2hu', '2hu & as'] + assert resp =~ FeedView.escape(object.data["content"]) + assert resp =~ FeedView.escape(object.data["summary"]) + assert resp =~ FeedView.escape(object.data["context"]) + assert resp =~ encoded_title + end end # Note: see ActivityPubControllerTest for JSON format tests diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index effa2144f..e87b33960 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -9,6 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do alias Pleroma.Repo alias Pleroma.Tests.ObanHelpers alias Pleroma.User + alias Pleroma.UserRelationship alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.InternalFetchActor alias Pleroma.Web.CommonAPI @@ -17,6 +18,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do import Pleroma.Factory + setup do + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + :ok + end + describe "account fetching" do test "works by id" do %User{id: user_id} = insert(:user) @@ -407,6 +413,20 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do assert id_two == to_string(activity.id) end + test "gets local-only statuses for authenticated users", %{user: _user, conn: conn} do + user_one = insert(:user) + + {:ok, activity} = CommonAPI.post(user_one, %{status: "HI!!!", visibility: "local"}) + + resp = + conn + |> get("/api/v1/accounts/#{user_one.id}/statuses") + |> json_response_and_validate_schema(200) + + assert [%{"id" => id}] = resp + assert id == to_string(activity.id) + end + test "gets an users media, excludes reblogs", %{conn: conn} do note = insert(:note_activity) user = User.get_cached_by_ap_id(note.data["actor"]) @@ -881,6 +901,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: true}) |> json_response_and_validate_schema(200) + assert %{"showing_reblogs" => true} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: "1"}) + |> json_response_and_validate_schema(200) + assert [%{"id" => ^reblog_id}] = conn |> get("/api/v1/timelines/home") @@ -910,6 +936,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: false}) |> json_response_and_validate_schema(200) + assert %{"showing_reblogs" => false} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: "0"}) + |> json_response_and_validate_schema(200) + assert [] == conn |> get("/api/v1/timelines/home") @@ -920,21 +952,23 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do %{conn: conn} = oauth_access(["follow"]) followed = insert(:user) - ret_conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/accounts/#{followed.id}/follow", %{notify: true}) - - assert %{"id" => _id, "subscribing" => true} = - json_response_and_validate_schema(ret_conn, 200) + assert %{"subscribing" => true} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/accounts/#{followed.id}/follow", %{notify: true}) + |> json_response_and_validate_schema(200) - ret_conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/accounts/#{followed.id}/follow", %{notify: false}) + assert %{"subscribing" => true} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/accounts/#{followed.id}/follow", %{notify: "1"}) + |> json_response_and_validate_schema(200) - assert %{"id" => _id, "subscribing" => false} = - json_response_and_validate_schema(ret_conn, 200) + assert %{"subscribing" => false} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/accounts/#{followed.id}/follow", %{notify: false}) + |> json_response_and_validate_schema(200) end test "following / unfollowing errors", %{user: user, conn: conn} do @@ -1011,6 +1045,40 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = json_response_and_validate_schema(conn, 200) end + + test "expiring", %{conn: conn, user: user} do + other_user = insert(:user) + + conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/accounts/#{other_user.id}/mute", %{"duration" => "86400"}) + + assert %{"id" => _id, "muting" => true} = json_response_and_validate_schema(conn, 200) + + mute_expires_at = UserRelationship.get_mute_expire_date(user, other_user) + + assert DateTime.diff( + mute_expires_at, + DateTime.utc_now() |> DateTime.add(24 * 60 * 60) + ) in -3..3 + end + + test "falls back to expires_in", %{conn: conn, user: user} do + other_user = insert(:user) + + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/accounts/#{other_user.id}/mute", %{"expires_in" => "86400"}) + |> json_response_and_validate_schema(200) + + mute_expires_at = UserRelationship.get_mute_expire_date(user, other_user) + + assert DateTime.diff( + mute_expires_at, + DateTime.utc_now() |> DateTime.add(24 * 60 * 60) + ) in -3..3 + end end describe "pinned statuses" do @@ -1292,7 +1360,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do assert user.registration_reason == "I'm a cool dude, bro" end - test "returns error when user already registred", %{conn: conn, valid_params: valid_params} do + test "returns error when user already registered", %{conn: conn, valid_params: valid_params} do _user = insert(:user, email: "lain@example.org") app_token = insert(:oauth_token, user: nil) @@ -1427,7 +1495,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do |> Plug.Conn.put_req_header("authorization", "Bearer " <> token) |> put_req_header("content-type", "multipart/form-data") |> post("/api/v1/accounts", %{ - nickname: "nickanme", + nickname: "nickname", agreement: true, email: "email@example.com", fullname: "Lain", @@ -1713,7 +1781,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do assert %{language: "ru_RU"} = Pleroma.User.get_by_nickname("foo") end - test "createing an account without language parameter should fallback to cookie/header language", + test "creating an account without language parameter should fallback to cookie/header language", %{conn: conn} do params = %{ username: "foo2", @@ -1829,21 +1897,21 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do |> get("/api/v1/mutes") |> json_response_and_validate_schema(200) - assert [id1, id2, id3] == Enum.map(result, & &1["id"]) + assert [id3, id2, id1] == Enum.map(result, & &1["id"]) result = conn |> get("/api/v1/mutes?limit=1") |> json_response_and_validate_schema(200) - assert [%{"id" => ^id1}] = result + assert [%{"id" => ^id3}] = result result = conn |> get("/api/v1/mutes?since_id=#{id1}") |> json_response_and_validate_schema(200) - assert [%{"id" => ^id2}, %{"id" => ^id3}] = result + assert [%{"id" => ^id3}, %{"id" => ^id2}] = result result = conn @@ -1857,7 +1925,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do |> get("/api/v1/mutes?since_id=#{id1}&limit=1") |> json_response_and_validate_schema(200) - assert [%{"id" => ^id2}] = result + assert [%{"id" => ^id3}] = result end test "list of mutes with with_relationships parameter" do @@ -1876,7 +1944,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do assert [ %{ - "id" => ^id1, + "id" => ^id3, "pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}} }, %{ @@ -1884,7 +1952,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do "pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}} }, %{ - "id" => ^id3, + "id" => ^id1, "pleroma" => %{"relationship" => %{"muting" => true, "followed_by" => true}} } ] = @@ -1909,7 +1977,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do |> get("/api/v1/blocks") |> json_response_and_validate_schema(200) - assert [id1, id2, id3] == Enum.map(result, & &1["id"]) + assert [id3, id2, id1] == Enum.map(result, & &1["id"]) result = conn @@ -1917,7 +1985,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do |> get("/api/v1/blocks?limit=1") |> json_response_and_validate_schema(200) - assert [%{"id" => ^id1}] = result + assert [%{"id" => ^id3}] = result result = conn @@ -1925,7 +1993,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do |> get("/api/v1/blocks?since_id=#{id1}") |> json_response_and_validate_schema(200) - assert [%{"id" => ^id2}, %{"id" => ^id3}] = result + assert [%{"id" => ^id3}, %{"id" => ^id2}] = result result = conn @@ -1941,7 +2009,64 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do |> get("/api/v1/blocks?since_id=#{id1}&limit=1") |> json_response_and_validate_schema(200) - assert [%{"id" => ^id2}] = result + assert [%{"id" => ^id3}] = result + + conn_res = + conn + |> assign(:user, user) + |> get("/api/v1/blocks?limit=2") + + next_url = + ~r{<.+?(?<link>/api[^>]+)>; rel=\"next\"} + |> Regex.named_captures(get_resp_header(conn_res, "link") |> Enum.at(0)) + |> Map.get("link") + + result = + conn_res + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^id3}, %{"id" => ^id2}] = result + + result = + conn + |> assign(:user, user) + |> get(next_url) + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^id1}] = result + end + + test "list of blocks with with_relationships parameter" do + %{user: user, conn: conn} = oauth_access(["read:blocks"]) + %{id: id1} = other_user1 = insert(:user) + %{id: id2} = other_user2 = insert(:user) + %{id: id3} = other_user3 = insert(:user) + + {:ok, _, _} = User.follow(other_user1, user) + {:ok, _, _} = User.follow(other_user2, user) + {:ok, _, _} = User.follow(other_user3, user) + + {:ok, _} = User.block(user, other_user1) + {:ok, _} = User.block(user, other_user2) + {:ok, _} = User.block(user, other_user3) + + assert [ + %{ + "id" => ^id3, + "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}} + }, + %{ + "id" => ^id2, + "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}} + }, + %{ + "id" => ^id1, + "pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}} + } + ] = + conn + |> get("/api/v1/blocks?with_relationships=true") + |> json_response_and_validate_schema(200) end test "account lookup", %{conn: conn} do @@ -2046,4 +2171,97 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do |> json_response_and_validate_schema(400) end end + + describe "familiar followers" do + setup do: oauth_access(["read:follows"]) + + test "fetch user familiar followers", %{user: user, conn: conn} do + %{id: id1} = other_user1 = insert(:user) + %{id: id2} = other_user2 = insert(:user) + _ = insert(:user) + + User.follow(user, other_user1) + User.follow(other_user1, other_user2) + + assert [%{"accounts" => [%{"id" => ^id1}], "id" => ^id2}] = + conn + |> put_req_header("content-type", "application/json") + |> get("/api/v1/accounts/familiar_followers?id[]=#{id2}") + |> json_response_and_validate_schema(200) + end + + test "returns empty array if followers are hidden", %{user: user, conn: conn} do + other_user1 = insert(:user, hide_follows: true) + %{id: id2} = other_user2 = insert(:user) + _ = insert(:user) + + User.follow(user, other_user1) + User.follow(other_user1, other_user2) + + assert [%{"accounts" => [], "id" => ^id2}] = + conn + |> put_req_header("content-type", "application/json") + |> get("/api/v1/accounts/familiar_followers?id[]=#{id2}") + |> json_response_and_validate_schema(200) + end + + test "it respects hide_followers", %{user: user, conn: conn} do + other_user1 = insert(:user) + %{id: id2} = other_user2 = insert(:user, hide_followers: true) + _ = insert(:user) + + User.follow(user, other_user1) + User.follow(other_user1, other_user2) + + assert [%{"accounts" => [], "id" => ^id2}] = + conn + |> put_req_header("content-type", "application/json") + |> get("/api/v1/accounts/familiar_followers?id[]=#{id2}") + |> json_response_and_validate_schema(200) + end + end + + describe "remove from followers" do + setup do: oauth_access(["follow"]) + + test "removing user from followers", %{conn: conn, user: user} do + %{id: other_user_id} = other_user = insert(:user) + + CommonAPI.follow(other_user, user) + + assert %{"id" => ^other_user_id, "followed_by" => false} = + conn + |> post("/api/v1/accounts/#{other_user_id}/remove_from_followers") + |> json_response_and_validate_schema(200) + + refute User.following?(other_user, user) + end + + test "removing remote user from followers", %{conn: conn, user: user} do + %{id: other_user_id} = other_user = insert(:user, local: false) + + CommonAPI.follow(other_user, user) + + assert User.following?(other_user, user) + + assert %{"id" => ^other_user_id, "followed_by" => false} = + conn + |> post("/api/v1/accounts/#{other_user_id}/remove_from_followers") + |> json_response_and_validate_schema(200) + + refute User.following?(other_user, user) + end + + test "removing user from followers errors", %{user: user, conn: conn} do + # self remove + conn_res = post(conn, "/api/v1/accounts/#{user.id}/remove_from_followers") + + assert %{"error" => "Can not unfollow yourself"} = + json_response_and_validate_schema(conn_res, 400) + + # remove non existing user + conn_res = post(conn, "/api/v1/accounts/doesntexist/remove_from_followers") + assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404) + end + end end diff --git a/test/pleroma/web/mastodon_api/controllers/directory_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/directory_controller_test.exs index f90ef96f9..40b23a5d6 100644 --- a/test/pleroma/web/mastodon_api/controllers/directory_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/directory_controller_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.MastodonAPI.DirectoryControllerTest do - use Pleroma.Web.ConnCase, async: true + use Pleroma.Web.ConnCase alias Pleroma.Web.CommonAPI import Pleroma.Factory diff --git a/test/pleroma/web/mastodon_api/controllers/filter_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/filter_controller_test.exs index ba4628fc5..faa35f199 100644 --- a/test/pleroma/web/mastodon_api/controllers/filter_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/filter_controller_test.exs @@ -3,9 +3,10 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do - use Pleroma.Web.ConnCase, async: true + use Pleroma.Web.ConnCase, async: false use Oban.Testing, repo: Pleroma.Repo + import Mock import Pleroma.Factory alias Pleroma.Filter @@ -53,24 +54,19 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do in_seconds = 600 response = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/filters", %{ - "phrase" => "knights", - context: ["home"], - expires_in: in_seconds - }) - |> json_response_and_validate_schema(200) + with_mock NaiveDateTime, [:passthrough], utc_now: fn -> ~N[2017-03-17 17:09:58] end do + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/filters", %{ + "phrase" => "knights", + context: ["home"], + expires_in: in_seconds + }) + |> json_response_and_validate_schema(200) + end assert response["irreversible"] == false - - expected_expiration = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(in_seconds) - - {:ok, actual_expiration} = NaiveDateTime.from_iso8601(response["expires_at"]) - - assert abs(NaiveDateTime.diff(expected_expiration, actual_expiration)) <= 5 + assert response["expires_at"] == "2017-03-17T17:19:58.000Z" filter = Filter.get(response["id"], user) @@ -177,28 +173,25 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do assert response["whole_word"] == true end - @tag :erratic test "with adding expires_at", %{conn: conn, user: user} do filter = insert(:filter, user: user) in_seconds = 600 response = - conn - |> put_req_header("content-type", "application/json") - |> put("/api/v1/filters/#{filter.filter_id}", %{ - phrase: "nii", - context: ["public"], - expires_in: in_seconds, - irreversible: true - }) - |> json_response_and_validate_schema(200) + with_mock NaiveDateTime, [:passthrough], utc_now: fn -> ~N[2017-03-17 17:09:58] end do + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/filters/#{filter.filter_id}", %{ + phrase: "nii", + context: ["public"], + expires_in: in_seconds, + irreversible: true + }) + |> json_response_and_validate_schema(200) + end assert response["irreversible"] == true - - assert response["expires_at"] == - NaiveDateTime.utc_now() - |> NaiveDateTime.add(in_seconds) - |> Pleroma.Web.CommonAPI.Utils.to_masto_date() + assert response["expires_at"] == "2017-03-17T17:19:58.000Z" filter = Filter.get(response["id"], user) diff --git a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs index 9845408d6..373a84303 100644 --- a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs @@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do # TODO: Should not need Cachex use Pleroma.Web.ConnCase + alias Pleroma.Rule alias Pleroma.User import Pleroma.Factory @@ -22,6 +23,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do "uri" => _, "title" => _, "description" => _, + "short_description" => _, "version" => _, "email" => from_config_email, "urls" => %{ @@ -39,7 +41,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do "banner_upload_limit" => _, "background_image" => from_config_background, "shout_limit" => _, - "description_limit" => _ + "description_limit" => _, + "rules" => _ } = result assert result["pleroma"]["metadata"]["account_activation_required"] != nil @@ -91,4 +94,62 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do assert ["peer1.com", "peer2.com"] == Enum.sort(result) end + + test "instance languages", %{conn: conn} do + assert %{"languages" => ["en"]} = + conn + |> get("/api/v1/instance") + |> json_response_and_validate_schema(200) + + clear_config([:instance, :languages], ["aa", "bb"]) + + assert %{"languages" => ["aa", "bb"]} = + conn + |> get("/api/v1/instance") + |> json_response_and_validate_schema(200) + end + + test "get instance contact information", %{conn: conn} do + user = insert(:user, %{local: true}) + + clear_config([:instance, :contact_username], user.nickname) + + conn = get(conn, "/api/v1/instance") + + assert result = json_response_and_validate_schema(conn, 200) + + assert result["contact_account"]["id"] == user.id + end + + test "get instance information v2", %{conn: conn} do + clear_config([:auth, :oauth_consumer_strategies], []) + + assert get(conn, "/api/v2/instance") + |> json_response_and_validate_schema(200) + end + + test "get instance rules", %{conn: conn} do + Rule.create(%{text: "Example rule", hint: "Rule description", priority: 1}) + Rule.create(%{text: "Third rule", priority: 2}) + Rule.create(%{text: "Second rule", priority: 1}) + + conn = get(conn, "/api/v1/instance") + + assert result = json_response_and_validate_schema(conn, 200) + + assert [ + %{ + "text" => "Example rule", + "hint" => "Rule description" + }, + %{ + "text" => "Second rule", + "hint" => "" + }, + %{ + "text" => "Third rule", + "hint" => "" + } + ] = result["rules"] + end end diff --git a/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs index 79d52bb2f..b92fd8afa 100644 --- a/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/media_controller_test.exs @@ -6,8 +6,10 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do use Pleroma.Web.ConnCase import ExUnit.CaptureLog + import Mox alias Pleroma.Object + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub @@ -15,6 +17,9 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do setup do: oauth_access(["write:media"]) setup do + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + image = %Plug.Upload{ content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), @@ -122,12 +127,32 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do assert :ok == File.rm(Path.absname("test/tmp/large_binary.data")) end + + test "Do not allow nested filename", %{conn: conn, image: image} do + image = %Plug.Upload{ + image + | filename: "../../../../../nested/file.jpg" + } + + desc = "Description of the image" + + media = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/v1/media", %{"file" => image, "description" => desc}) + |> json_response_and_validate_schema(:ok) + + refute Regex.match?(~r"/nested/", media["url"]) + end end describe "Update media description" do setup do: oauth_access(["write:media"]) setup %{user: actor} do + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + file = %Plug.Upload{ content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), @@ -160,6 +185,9 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do setup do: oauth_access(["read:media"]) setup %{user: actor} do + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + file = %Plug.Upload{ content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), diff --git a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs index 2b7a95635..350b935d7 100644 --- a/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do - use Pleroma.Web.ConnCase + use Pleroma.Web.ConnCase, async: false alias Pleroma.Notification alias Pleroma.Repo @@ -12,6 +12,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do import Pleroma.Factory + setup do + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + :ok + end + test "does NOT render account/pleroma/relationship by default" do %{user: user, conn: conn} = oauth_access(["read:notifications"]) other_user = insert(:user) @@ -74,12 +79,15 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do end test "by default, does not contain pleroma:report" do - %{user: user, conn: conn} = oauth_access(["read:notifications"]) + clear_config([:instance, :moderator_privileges], [:reports_manage_reports]) + + user = insert(:user) other_user = insert(:user) third_user = insert(:user) - user - |> User.admin_api_update(%{is_moderator: true}) + {:ok, user} = user |> User.admin_api_update(%{is_moderator: true}) + + %{conn: conn} = oauth_access(["read:notifications"], user: user) {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) @@ -101,6 +109,39 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do assert [_] = result end + test "Pleroma:report is hidden for non-privileged users" do + clear_config([:instance, :moderator_privileges], [:reports_manage_reports]) + + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, user} = user |> User.admin_api_update(%{is_moderator: true}) + + %{conn: conn} = oauth_access(["read:notifications"], user: user) + + {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) + + {:ok, _report} = + CommonAPI.report(third_user, %{account_id: other_user.id, status_ids: [activity.id]}) + + result = + conn + |> get("/api/v1/notifications?include_types[]=pleroma:report") + |> json_response_and_validate_schema(200) + + assert [_] = result + + clear_config([:instance, :moderator_privileges], []) + + result = + conn + |> get("/api/v1/notifications?include_types[]=pleroma:report") + |> json_response_and_validate_schema(200) + + assert [] == result + end + test "excludes mentions from blockers when blockers_visible is false" do clear_config([:activitypub, :blockers_visible], false) @@ -423,7 +464,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do assert [%{"id" => ^reblog_notification_id}] = json_response_and_validate_schema(conn_res, 200) end - test "filters notifications using include_types" do + test "filters notifications using types" do %{user: user, conn: conn} = oauth_access(["read:notifications"]) other_user = insert(:user) @@ -438,21 +479,21 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do reblog_notification_id = get_notification_id_by_activity(reblog_activity) follow_notification_id = get_notification_id_by_activity(follow_activity) - conn_res = get(conn, "/api/v1/notifications?include_types[]=follow") + conn_res = get(conn, "/api/v1/notifications?types[]=follow") assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200) - conn_res = get(conn, "/api/v1/notifications?include_types[]=mention") + conn_res = get(conn, "/api/v1/notifications?types[]=mention") assert [%{"id" => ^mention_notification_id}] = json_response_and_validate_schema(conn_res, 200) - conn_res = get(conn, "/api/v1/notifications?include_types[]=favourite") + conn_res = get(conn, "/api/v1/notifications?types[]=favourite") assert [%{"id" => ^favorite_notification_id}] = json_response_and_validate_schema(conn_res, 200) - conn_res = get(conn, "/api/v1/notifications?include_types[]=reblog") + conn_res = get(conn, "/api/v1/notifications?types[]=reblog") assert [%{"id" => ^reblog_notification_id}] = json_response_and_validate_schema(conn_res, 200) @@ -460,7 +501,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do assert length(result) == 4 - query = params_to_query(%{include_types: ["follow", "mention", "favourite", "reblog"]}) + query = params_to_query(%{types: ["follow", "mention", "favourite", "reblog"]}) result = conn @@ -470,6 +511,23 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do assert length(result) == 4 end + test "filtering falls back to include_types" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, _activity} = CommonAPI.post(other_user, %{status: "hey @#{user.nickname}"}) + {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"}) + {:ok, _activity} = CommonAPI.favorite(other_user, create_activity.id) + {:ok, _activity} = CommonAPI.repeat(create_activity.id, other_user) + {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user) + + follow_notification_id = get_notification_id_by_activity(follow_activity) + + conn_res = get(conn, "/api/v1/notifications?include_types[]=follow") + + assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200) + end + test "destroy multiple" do %{user: user, conn: conn} = oauth_access(["read:notifications", "write:notifications"]) other_user = insert(:user) diff --git a/test/pleroma/web/mastodon_api/controllers/report_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/report_controller_test.exs index 6d1a63334..4ab5d0771 100644 --- a/test/pleroma/web/mastodon_api/controllers/report_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/report_controller_test.exs @@ -5,6 +5,9 @@ defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do use Pleroma.Web.ConnCase, async: true + alias Pleroma.Activity + alias Pleroma.Repo + alias Pleroma.Rule alias Pleroma.Web.CommonAPI import Pleroma.Factory @@ -27,6 +30,41 @@ defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do |> json_response_and_validate_schema(200) end + test "submit a report with a fake Create", %{ + conn: conn + } do + target_user = insert(:user) + + note = insert(:note, user: target_user) + + activity_params = %{ + "object" => note.data["id"], + "actor" => note.data["actor"], + "to" => note.data["to"] || [], + "cc" => note.data["cc"] || [], + "type" => "Create" + } + + {:ok, fake_activity} = + Repo.insert(%Activity{ + data: activity_params, + recipients: activity_params["to"] ++ activity_params["cc"], + local: true, + actor: activity_params["actor"] + }) + + assert %{"action_taken" => false, "id" => _} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/reports", %{ + "account_id" => target_user.id, + "status_ids" => [fake_activity.id], + "comment" => "bad status!", + "forward" => "false" + }) + |> json_response_and_validate_schema(200) + end + test "submit a report with statuses and comment", %{ conn: conn, target_user: target_user, @@ -44,6 +82,44 @@ defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do |> json_response_and_validate_schema(200) end + test "submit a report with rule_ids", %{ + conn: conn, + target_user: target_user + } do + %{id: rule_id} = Rule.create(%{text: "There are no rules"}) + + rule_id = to_string(rule_id) + + assert %{"action_taken" => false, "id" => id} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/reports", %{ + "account_id" => target_user.id, + "forward" => "false", + "rule_ids" => [rule_id] + }) + |> json_response_and_validate_schema(200) + + assert %Activity{data: %{"rules" => [^rule_id]}} = Activity.get_report(id) + end + + test "rules field is empty if provided wrong rule id", %{ + conn: conn, + target_user: target_user + } do + assert %{"id" => id} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/reports", %{ + "account_id" => target_user.id, + "forward" => "false", + "rule_ids" => ["-1"] + }) + |> json_response_and_validate_schema(200) + + assert %Activity{data: %{"rules" => []}} = Activity.get_report(id) + end + test "account_id is required", %{ conn: conn, activity: activity diff --git a/test/pleroma/web/mastodon_api/controllers/scheduled_activity_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/scheduled_activity_controller_test.exs index 21f2ea6f5..632242221 100644 --- a/test/pleroma/web/mastodon_api/controllers/scheduled_activity_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/scheduled_activity_controller_test.exs @@ -3,15 +3,25 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do - use Pleroma.Web.ConnCase + use Pleroma.Web.ConnCase, async: true alias Pleroma.Repo alias Pleroma.ScheduledActivity + alias Pleroma.UnstubbedConfigMock, as: ConfigMock - import Pleroma.Factory import Ecto.Query + import Mox + import Pleroma.Factory - setup do: clear_config([ScheduledActivity, :enabled]) + setup do + ConfigMock + |> stub(:get, fn + [ScheduledActivity, :enabled] -> true + path -> Pleroma.Test.StaticConfig.get(path) + end) + + :ok + end test "shows scheduled activities" do %{user: user, conn: conn} = oauth_access(["read:statuses"]) @@ -55,7 +65,6 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do end test "updates a scheduled activity" do - clear_config([ScheduledActivity, :enabled], true) %{user: user, conn: conn} = oauth_access(["write:statuses"]) scheduled_at = Timex.shift(NaiveDateTime.utc_now(), minutes: 60) @@ -103,7 +112,6 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do end test "deletes a scheduled activity" do - clear_config([ScheduledActivity, :enabled], true) %{user: user, conn: conn} = oauth_access(["write:statuses"]) scheduled_at = Timex.shift(NaiveDateTime.utc_now(), minutes: 60) diff --git a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs index 8753c7716..ad4144da4 100644 --- a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs @@ -13,6 +13,11 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do import Tesla.Mock import Mock + setup do + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + :ok + end + setup_all do mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) :ok @@ -37,6 +42,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do end end + @tag :skip_darwin test "search", %{conn: conn} do user = insert(:user) user_two = insert(:user, %{nickname: "shp@shitposter.club"}) @@ -79,6 +85,51 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do assert status["id"] == to_string(activity.id) end + test "search local-only status as an authenticated user" do + user = insert(:user) + %{conn: conn} = oauth_access(["read:search"]) + + {:ok, activity} = + CommonAPI.post(user, %{status: "This is about 2hu private 天子", visibility: "local"}) + + results = + conn + |> get("/api/v2/search?#{URI.encode_query(%{q: "2hu"})}") + |> json_response_and_validate_schema(200) + + [status] = results["statuses"] + assert status["id"] == to_string(activity.id) + end + + test "search local-only status as an unauthenticated user" do + user = insert(:user) + %{conn: conn} = oauth_access([]) + + {:ok, _activity} = + CommonAPI.post(user, %{status: "This is about 2hu private 天子", visibility: "local"}) + + results = + conn + |> get("/api/v2/search?#{URI.encode_query(%{q: "2hu"})}") + |> json_response_and_validate_schema(200) + + assert [] = results["statuses"] + end + + test "search local-only status as an anonymous user" do + user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(user, %{status: "This is about 2hu private 天子", visibility: "local"}) + + results = + build_conn() + |> get("/api/v2/search?#{URI.encode_query(%{q: "2hu"})}") + |> json_response_and_validate_schema(200) + + assert [] = results["statuses"] + end + @tag capture_log: true test "constructs hashtags from search query", %{conn: conn} do results = @@ -271,26 +322,20 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do end test "search fetches remote statuses and prefers them over other results", %{conn: conn} do - old_version = :persistent_term.get({Pleroma.Repo, :postgres_version}) - :persistent_term.put({Pleroma.Repo, :postgres_version}, 10.0) - on_exit(fn -> :persistent_term.put({Pleroma.Repo, :postgres_version}, old_version) end) - - capture_log(fn -> - {:ok, %{id: activity_id}} = - CommonAPI.post(insert(:user), %{ - status: "check out http://mastodon.example.org/@admin/99541947525187367" - }) + {:ok, %{id: activity_id}} = + CommonAPI.post(insert(:user), %{ + status: "check out http://mastodon.example.org/@admin/99541947525187367" + }) - results = - conn - |> get("/api/v1/search?q=http://mastodon.example.org/@admin/99541947525187367") - |> json_response_and_validate_schema(200) + %{"url" => result_url, "id" => result_id} = + conn + |> get("/api/v1/search?q=http://mastodon.example.org/@admin/99541947525187367") + |> json_response_and_validate_schema(200) + |> Map.get("statuses") + |> List.first() - assert [ - %{"url" => "http://mastodon.example.org/@admin/99541947525187367"}, - %{"id" => ^activity_id} - ] = results["statuses"] - end) + refute match?(^result_id, activity_id) + assert match?(^result_url, "http://mastodon.example.org/@admin/99541947525187367") end test "search doesn't show statuses that it shouldn't", %{conn: conn} do diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index dc6912b7b..f34911e5b 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -3,14 +3,16 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do - use Pleroma.Web.ConnCase + use Pleroma.Web.ConnCase, async: false use Oban.Testing, repo: Pleroma.Repo alias Pleroma.Activity alias Pleroma.Conversation.Participation + alias Pleroma.ModerationLog alias Pleroma.Object alias Pleroma.Repo alias Pleroma.ScheduledActivity + alias Pleroma.Tests.Helpers alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub @@ -18,25 +20,38 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do alias Pleroma.Web.CommonAPI alias Pleroma.Workers.ScheduledActivityWorker + import Mox import Pleroma.Factory setup do: clear_config([:instance, :federating]) setup do: clear_config([:instance, :allow_relay]) - setup do: clear_config([:rich_media, :enabled]) setup do: clear_config([:mrf, :policies]) setup do: clear_config([:mrf_keyword, :reject]) + setup do + Pleroma.UnstubbedConfigMock + |> stub_with(Pleroma.Config) + + Pleroma.StaticStubbedConfigMock + |> stub(:get, fn + [:rich_media, :enabled] -> false + path -> Pleroma.Test.StaticConfig.get(path) + end) + + :ok + end + describe "posting statuses" do setup do: oauth_access(["write:statuses"]) test "posting a status does not increment reblog_count when relaying", %{conn: conn} do clear_config([:instance, :federating], true) - Config.get([:instance, :allow_relay], true) + clear_config([:instance, :allow_relay], true) response = conn |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{ + |> post("/api/v1/statuses", %{ "content_type" => "text/plain", "source" => "Pleroma FE", "status" => "Hello world", @@ -49,7 +64,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do response = conn - |> get("api/v1/statuses/#{response["id"]}", %{}) + |> get("/api/v1/statuses/#{response["id"]}", %{}) |> json_response_and_validate_schema(200) assert response["reblogs_count"] == 0 @@ -108,7 +123,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do conn_four = conn |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{ + |> post("/api/v1/statuses", %{ "status" => "oolong", "expires_in" => expires_in }) @@ -124,6 +139,28 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do ) end + test "posting a quote post", %{conn: conn} do + user = insert(:user) + + {:ok, %{id: activity_id} = activity} = CommonAPI.post(user, %{status: "yolo"}) + %{data: %{"id" => quote_url}} = Object.normalize(activity) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "indeed", + "quote_id" => activity_id + }) + + assert %{ + "id" => id, + "pleroma" => %{"quote" => %{"id" => ^activity_id}, "quote_url" => ^quote_url} + } = json_response_and_validate_schema(conn, 200) + + assert Activity.get_by_id(id) + end + test "it fails to create a status if `expires_in` is less or equal than an hour", %{ conn: conn } do @@ -133,7 +170,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do assert %{"error" => "Expiry date is too soon"} = conn |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{ + |> post("/api/v1/statuses", %{ "status" => "oolong", "expires_in" => expires_in }) @@ -145,7 +182,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do assert %{"error" => "Expiry date is too soon"} = conn |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{ + |> post("/api/v1/statuses", %{ "status" => "oolong", "expires_in" => expires_in }) @@ -159,7 +196,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} = conn |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{"status" => "GNO/Linux"}) + |> post("/api/v1/statuses", %{"status" => "GNO/Linux"}) |> json_response_and_validate_schema(422) end @@ -198,6 +235,16 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do assert Activity.get_in_reply_to_activity(activity).id == replied_to.id end + test "replying to a deleted status", %{user: user, conn: conn} do + {:ok, status} = CommonAPI.post(user, %{status: "cofe"}) + {:ok, _deleted_status} = CommonAPI.delete(status.id, user) + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => status.id}) + |> json_response_and_validate_schema(422) + end + test "replying to a direct message with visibility other than direct", %{ user: user, conn: conn @@ -262,6 +309,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do |> Map.put("url", nil) |> Map.put("uri", nil) |> Map.put("created_at", nil) + |> Kernel.put_in(["pleroma", "context"], nil) |> Kernel.put_in(["pleroma", "conversation_id"], nil) fake_conn = @@ -285,64 +333,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do |> Map.put("url", nil) |> Map.put("uri", nil) |> Map.put("created_at", nil) + |> Kernel.put_in(["pleroma", "context"], nil) |> Kernel.put_in(["pleroma", "conversation_id"], nil) assert real_status == fake_status end - test "fake statuses' preview card is not cached", %{conn: conn} do - clear_config([:rich_media, :enabled], true) - - Tesla.Mock.mock(fn - %{ - method: :get, - url: "https://example.com/twitter-card" - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")} - - env -> - apply(HttpRequestMock, :request, [env]) - end) - - conn1 = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "https://example.com/ogp", - "preview" => true - }) - - conn2 = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "https://example.com/twitter-card", - "preview" => true - }) - - assert %{"card" => %{"title" => "The Rock"}} = json_response_and_validate_schema(conn1, 200) - - assert %{"card" => %{"title" => "Small Island Developing States Photo Submission"}} = - json_response_and_validate_schema(conn2, 200) - end - - test "posting a status with OGP link preview", %{conn: conn} do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - clear_config([:rich_media, :enabled], true) - - conn = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "https://example.com/ogp" - }) - - assert %{"id" => id, "card" => %{"title" => "The Rock"}} = - json_response_and_validate_schema(conn, 200) - - assert Activity.get_by_id(id) - end - test "posting a direct status", %{conn: conn} do user2 = insert(:user) content = "direct cofe @#{user2.nickname}" @@ -350,7 +346,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do conn = conn |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"}) + |> post("/api/v1/statuses", %{"status" => content, "visibility" => "direct"}) assert %{"id" => id} = response = json_response_and_validate_schema(conn, 200) assert response["visibility"] == "direct" @@ -387,7 +383,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do result = conn - |> get("api/v1/statuses/#{activity}") + |> get("/api/v1/statuses/#{activity}") assert %{ "content" => "cofe is my copilot", @@ -416,7 +412,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do result = conn - |> get("api/v1/statuses/#{activity}") + |> get("/api/v1/statuses/#{activity}") assert %{ "content" => "club mate is my wingman", @@ -623,7 +619,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do |> put_req_header("content-type", "application/json") |> post("/api/v1/statuses", %{ "status" => "desu~", - "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1} + "poll" => %{ + "options" => Enum.map(0..limit, fn num -> "desu #{num}" end), + "expires_in" => 1 + } }) %{"error" => error} = json_response_and_validate_schema(conn, 422) @@ -639,7 +638,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do |> post("/api/v1/statuses", %{ "status" => "...", "poll" => %{ - "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)], + "options" => [String.duplicate(".", limit + 1), "lol"], "expires_in" => 1 } }) @@ -721,6 +720,32 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do assert object.data["type"] == "Question" assert length(object.data["oneOf"]) == 3 end + + test "cannot have only one option", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "desu~", + "poll" => %{"options" => ["mew"], "expires_in" => 1} + }) + + %{"error" => error} = json_response_and_validate_schema(conn, 422) + assert error == "Poll must contain at least 2 options" + end + + test "cannot have only duplicated options", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "desu~", + "poll" => %{"options" => ["mew", "mew"], "expires_in" => 1} + }) + + %{"error" => error} = json_response_and_validate_schema(conn, 422) + assert error == "Poll must contain at least 2 options" + end end test "get a status" do @@ -739,6 +764,49 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do {:ok, local: local, remote: remote} end + defp local_and_remote_context_activities do + local_user_1 = insert(:user) + local_user_2 = insert(:user) + remote_user = insert(:user, local: false) + + {:ok, %{id: id1, data: %{"context" => context}}} = + CommonAPI.post(local_user_1, %{status: "post"}) + + {:ok, %{id: id2} = post} = + CommonAPI.post(local_user_2, %{status: "local reply", in_reply_to_status_id: id1}) + + params = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "actor" => remote_user.ap_id, + "type" => "Create", + "context" => context, + "id" => "#{remote_user.ap_id}/activities/1", + "inReplyTo" => post.data["id"], + "object" => %{ + "type" => "Note", + "content" => "remote reply", + "context" => context, + "id" => "#{remote_user.ap_id}/objects/1", + "attributedTo" => remote_user.ap_id, + "to" => [ + local_user_1.ap_id, + local_user_2.ap_id, + "https://www.w3.org/ns/activitystreams#Public" + ] + }, + "to" => [ + local_user_1.ap_id, + local_user_2.ap_id, + "https://www.w3.org/ns/activitystreams#Public" + ] + } + + {:ok, job} = Pleroma.Web.Federator.incoming_ap_doc(params) + {:ok, remote_activity} = ObanHelpers.perform(job) + + %{locals: [id1, id2], remote: remote_activity.id, context: context} + end + describe "status with restrict unauthenticated activities for local and remote" do setup do: local_and_remote_activities() @@ -925,6 +993,230 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do end end + describe "getting status contexts restricted unauthenticated for local and remote" do + setup do: local_and_remote_context_activities() + + setup do: clear_config([:restrict_unauthenticated, :activities, :local], true) + + setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true) + + test "if user is unauthenticated", %{conn: conn, locals: [post_id, _]} do + res_conn = get(conn, "/api/v1/statuses/#{post_id}/context") + + assert json_response_and_validate_schema(res_conn, 200) == %{ + "ancestors" => [], + "descendants" => [] + } + end + + test "if user is unauthenticated reply", %{conn: conn, locals: [_, reply_id]} do + res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context") + + assert json_response_and_validate_schema(res_conn, 200) == %{ + "ancestors" => [], + "descendants" => [] + } + end + + test "if user is authenticated", %{locals: [post_id, reply_id], remote: remote_reply_id} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{post_id}/context") + + %{"ancestors" => [], "descendants" => descendants} = + json_response_and_validate_schema(res_conn, 200) + + descendant_ids = + descendants + |> Enum.map(& &1["id"]) + + assert reply_id in descendant_ids + assert remote_reply_id in descendant_ids + end + + test "if user is authenticated reply", %{locals: [post_id, reply_id], remote: remote_reply_id} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context") + + %{"ancestors" => ancestors, "descendants" => descendants} = + json_response_and_validate_schema(res_conn, 200) + + ancestor_ids = + ancestors + |> Enum.map(& &1["id"]) + + descendant_ids = + descendants + |> Enum.map(& &1["id"]) + + assert post_id in ancestor_ids + assert remote_reply_id in descendant_ids + end + end + + describe "getting status contexts restricted unauthenticated for local" do + setup do: local_and_remote_context_activities() + + setup do: clear_config([:restrict_unauthenticated, :activities, :local], true) + + setup do: clear_config([:restrict_unauthenticated, :activities, :remote], false) + + test "if user is unauthenticated", %{ + conn: conn, + locals: [post_id, reply_id], + remote: remote_reply_id + } do + res_conn = get(conn, "/api/v1/statuses/#{post_id}/context") + + %{"ancestors" => [], "descendants" => descendants} = + json_response_and_validate_schema(res_conn, 200) + + descendant_ids = + descendants + |> Enum.map(& &1["id"]) + + assert reply_id not in descendant_ids + assert remote_reply_id in descendant_ids + end + + test "if user is unauthenticated reply", %{ + conn: conn, + locals: [post_id, reply_id], + remote: remote_reply_id + } do + res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context") + + %{"ancestors" => ancestors, "descendants" => descendants} = + json_response_and_validate_schema(res_conn, 200) + + ancestor_ids = + ancestors + |> Enum.map(& &1["id"]) + + descendant_ids = + descendants + |> Enum.map(& &1["id"]) + + assert post_id not in ancestor_ids + assert remote_reply_id in descendant_ids + end + + test "if user is authenticated", %{locals: [post_id, reply_id], remote: remote_reply_id} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{post_id}/context") + + %{"ancestors" => [], "descendants" => descendants} = + json_response_and_validate_schema(res_conn, 200) + + descendant_ids = + descendants + |> Enum.map(& &1["id"]) + + assert reply_id in descendant_ids + assert remote_reply_id in descendant_ids + end + + test "if user is authenticated reply", %{locals: [post_id, reply_id], remote: remote_reply_id} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context") + + %{"ancestors" => ancestors, "descendants" => descendants} = + json_response_and_validate_schema(res_conn, 200) + + ancestor_ids = + ancestors + |> Enum.map(& &1["id"]) + + descendant_ids = + descendants + |> Enum.map(& &1["id"]) + + assert post_id in ancestor_ids + assert remote_reply_id in descendant_ids + end + end + + describe "getting status contexts restricted unauthenticated for remote" do + setup do: local_and_remote_context_activities() + + setup do: clear_config([:restrict_unauthenticated, :activities, :local], false) + + setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true) + + test "if user is unauthenticated", %{ + conn: conn, + locals: [post_id, reply_id], + remote: remote_reply_id + } do + res_conn = get(conn, "/api/v1/statuses/#{post_id}/context") + + %{"ancestors" => [], "descendants" => descendants} = + json_response_and_validate_schema(res_conn, 200) + + descendant_ids = + descendants + |> Enum.map(& &1["id"]) + + assert reply_id in descendant_ids + assert remote_reply_id not in descendant_ids + end + + test "if user is unauthenticated reply", %{ + conn: conn, + locals: [post_id, reply_id], + remote: remote_reply_id + } do + res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context") + + %{"ancestors" => ancestors, "descendants" => descendants} = + json_response_and_validate_schema(res_conn, 200) + + ancestor_ids = + ancestors + |> Enum.map(& &1["id"]) + + descendant_ids = + descendants + |> Enum.map(& &1["id"]) + + assert post_id in ancestor_ids + assert remote_reply_id not in descendant_ids + end + + test "if user is authenticated", %{locals: [post_id, reply_id], remote: remote_reply_id} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{post_id}/context") + + %{"ancestors" => [], "descendants" => descendants} = + json_response_and_validate_schema(res_conn, 200) + + reply_ids = + descendants + |> Enum.map(& &1["id"]) + + assert reply_id in reply_ids + assert remote_reply_id in reply_ids + end + + test "if user is authenticated reply", %{locals: [post_id, reply_id], remote: remote_reply_id} do + %{conn: conn} = oauth_access(["read"]) + res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context") + + %{"ancestors" => ancestors, "descendants" => descendants} = + json_response_and_validate_schema(res_conn, 200) + + ancestor_ids = + ancestors + |> Enum.map(& &1["id"]) + + descendant_ids = + descendants + |> Enum.map(& &1["id"]) + + assert post_id in ancestor_ids + assert remote_reply_id in descendant_ids + end + end + describe "deleting a status" do test "when you created it" do %{user: author, conn: conn} = oauth_access(["write:statuses"]) @@ -968,30 +1260,44 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do assert Activity.get_by_id(activity.id) == activity end - test "when you're an admin or moderator", %{conn: conn} do - activity1 = insert(:note_activity) - activity2 = insert(:note_activity) - admin = insert(:user, is_admin: true) - moderator = insert(:user, is_moderator: true) + test "when you're privileged to", %{conn: conn} do + clear_config([:instance, :moderator_privileges], [:messages_delete]) + activity = insert(:note_activity) + user = insert(:user, is_moderator: true) res_conn = conn - |> assign(:user, admin) - |> assign(:token, insert(:oauth_token, user: admin, scopes: ["write:statuses"])) - |> delete("/api/v1/statuses/#{activity1.id}") + |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:statuses"])) + |> delete("/api/v1/statuses/#{activity.id}") assert %{} = json_response_and_validate_schema(res_conn, 200) + assert ModerationLog |> Repo.one() |> ModerationLog.get_log_entry_message() == + "@#{user.nickname} deleted status ##{activity.id}" + + refute Activity.get_by_id(activity.id) + end + + test "when you're privileged and the user is banned", %{conn: conn} do + clear_config([:instance, :moderator_privileges], [:messages_delete]) + posting_user = insert(:user, is_active: false) + refute posting_user.is_active + activity = insert(:note_activity, user: posting_user) + user = insert(:user, is_moderator: true) + res_conn = conn - |> assign(:user, moderator) - |> assign(:token, insert(:oauth_token, user: moderator, scopes: ["write:statuses"])) - |> delete("/api/v1/statuses/#{activity2.id}") + |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:statuses"])) + |> delete("/api/v1/statuses/#{activity.id}") assert %{} = json_response_and_validate_schema(res_conn, 200) - refute Activity.get_by_id(activity1.id) - refute Activity.get_by_id(activity2.id) + assert ModerationLog |> Repo.one() |> ModerationLog.get_log_entry_message() == + "@#{user.nickname} deleted status ##{activity.id}" + + refute Activity.get_by_id(activity.id) end end @@ -1309,7 +1615,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do assert %{"id" => id} = conn |> put_req_header("content-type", "application/json") - |> post("api/v1/statuses", %{ + |> post("/api/v1/statuses", %{ "status" => "oolong", "expires_in" => expires_in }) @@ -1347,87 +1653,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do end end - describe "cards" do - setup do - clear_config([:rich_media, :enabled], true) - - oauth_access(["read:statuses"]) - end - - test "returns rich-media card", %{conn: conn, user: user} do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - - {:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp"}) - - card_data = %{ - "image" => "http://ia.media-imdb.com/images/rock.jpg", - "provider_name" => "example.com", - "provider_url" => "https://example.com", - "title" => "The Rock", - "type" => "link", - "url" => "https://example.com/ogp", - "description" => - "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.", - "pleroma" => %{ - "opengraph" => %{ - "image" => "http://ia.media-imdb.com/images/rock.jpg", - "title" => "The Rock", - "type" => "video.movie", - "url" => "https://example.com/ogp", - "description" => - "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer." - } - } - } - - response = - conn - |> get("/api/v1/statuses/#{activity.id}/card") - |> json_response_and_validate_schema(200) - - assert response == card_data - - # works with private posts - {:ok, activity} = - CommonAPI.post(user, %{status: "https://example.com/ogp", visibility: "direct"}) - - response_two = - conn - |> get("/api/v1/statuses/#{activity.id}/card") - |> json_response_and_validate_schema(200) - - assert response_two == card_data - end - - test "replaces missing description with an empty string", %{conn: conn, user: user} do - Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) - - {:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp-missing-data"}) - - response = - conn - |> get("/api/v1/statuses/#{activity.id}/card") - |> json_response_and_validate_schema(:ok) - - assert response == %{ - "type" => "link", - "title" => "Pleroma", - "description" => "", - "image" => nil, - "provider_name" => "example.com", - "provider_url" => "https://example.com", - "url" => "https://example.com/ogp-missing-data", - "pleroma" => %{ - "opengraph" => %{ - "title" => "Pleroma", - "type" => "website", - "url" => "https://example.com/ogp-missing-data" - } - } - } - end - end - test "bookmarks" do bookmarks_uri = "/api/v1/bookmarks" @@ -1472,6 +1697,60 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do json_response_and_validate_schema(bookmarks, 200) end + test "bookmark folders" do + %{conn: conn, user: user} = oauth_access(["write:bookmarks", "read:bookmarks"]) + + {:ok, folder} = Pleroma.BookmarkFolder.create(user.id, "folder") + author = insert(:user) + + folder_bookmarks_uri = "/api/v1/bookmarks?folder_id=#{folder.id}" + + {:ok, activity1} = CommonAPI.post(author, %{status: "heweoo?"}) + {:ok, activity2} = CommonAPI.post(author, %{status: "heweoo!"}) + + # Add bookmark with a folder + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity1.id}/bookmark", %{folder_id: folder.id}) + + assert json_response_and_validate_schema(response, 200)["bookmarked"] == true + + assert json_response_and_validate_schema(response, 200)["pleroma"]["bookmark_folder"] == + folder.id + + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity2.id}/bookmark") + + assert json_response_and_validate_schema(response, 200)["bookmarked"] == true + assert json_response_and_validate_schema(response, 200)["pleroma"]["bookmark_folder"] == nil + + bookmarks = + get(conn, folder_bookmarks_uri) + |> json_response_and_validate_schema(200) + + assert length(bookmarks) == 1 + + # Update folder for existing bookmark + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses/#{activity2.id}/bookmark", %{folder_id: folder.id}) + + assert json_response_and_validate_schema(response, 200)["bookmarked"] == true + + assert json_response_and_validate_schema(response, 200)["pleroma"]["bookmark_folder"] == + folder.id + + bookmarks = + get(conn, folder_bookmarks_uri) + |> json_response_and_validate_schema(200) + + assert length(bookmarks) == 2 + end + describe "conversation muting" do setup do: oauth_access(["write:mutes"]) @@ -1559,7 +1838,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do conn |> assign(:user, user3) |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"])) - |> get("api/v1/timelines/home") + |> get("/api/v1/timelines/home") [reblogged_activity] = json_response_and_validate_schema(conn3, 200) @@ -1830,7 +2109,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do # Using the header for pagination works correctly [next, _] = get_resp_header(result, "link") |> hd() |> String.split(", ") - [_, max_id] = Regex.run(~r/max_id=([^&]+)/, next) + [next_url, _next_rel] = String.split(next, ";") + next_url = String.trim_trailing(next_url, ">") |> String.trim_leading("<") + + max_id = Helpers.get_query_parameter(next_url, "max_id") assert max_id == third_favorite.id @@ -1901,23 +2183,50 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do |> json_response_and_validate_schema(:ok) end - test "posting a local only status" do - %{user: _user, conn: conn} = oauth_access(["write:statuses"]) + describe "local-only statuses" do + test "posting a local only status" do + %{user: _user, conn: conn} = oauth_access(["write:statuses"]) - conn_one = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/statuses", %{ - "status" => "cofe", - "visibility" => "local" - }) + conn_one = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/statuses", %{ + "status" => "cofe", + "visibility" => "local" + }) + + local = Utils.as_local_public() + + assert %{"content" => "cofe", "id" => id, "visibility" => "local"} = + json_response_and_validate_schema(conn_one, 200) - local = Utils.as_local_public() + assert %Activity{id: ^id, data: %{"to" => [^local]}} = Activity.get_by_id(id) + end + + test "other users can read local-only posts" do + user = insert(:user) + %{user: _reader, conn: conn} = oauth_access(["read:statuses"]) + + {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"}) - assert %{"content" => "cofe", "id" => id, "visibility" => "local"} = - json_response_and_validate_schema(conn_one, 200) + received = + conn + |> get("/api/v1/statuses/#{activity.id}") + |> json_response_and_validate_schema(:ok) - assert %Activity{id: ^id, data: %{"to" => [^local]}} = Activity.get_by_id(id) + assert received["id"] == activity.id + end + + test "anonymous users cannot see local-only posts" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"}) + + _received = + build_conn() + |> get("/api/v1/statuses/#{activity.id}") + |> json_response_and_validate_schema(:not_found) + end end describe "muted reactions" do @@ -1990,4 +2299,178 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do } = result end end + + describe "get status history" do + setup do + %{conn: build_conn()} + end + + test "unedited post", %{conn: conn} do + activity = insert(:note_activity) + + conn = get(conn, "/api/v1/statuses/#{activity.id}/history") + + assert [_] = json_response_and_validate_schema(conn, 200) + end + + test "edited post", %{conn: conn} do + note = + insert( + :note, + data: %{ + "formerRepresentations" => %{ + "type" => "OrderedCollection", + "orderedItems" => [ + %{ + "type" => "Note", + "content" => "mew mew 2", + "summary" => "title 2" + }, + %{ + "type" => "Note", + "content" => "mew mew 1", + "summary" => "title 1" + } + ], + "totalItems" => 2 + } + } + ) + + activity = insert(:note_activity, note: note) + + conn = get(conn, "/api/v1/statuses/#{activity.id}/history") + + assert [%{"spoiler_text" => "title 1"}, %{"spoiler_text" => "title 2"}, _] = + json_response_and_validate_schema(conn, 200) + end + end + + describe "get status source" do + setup do + %{conn: build_conn()} + end + + test "it returns the source", %{conn: conn} do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"}) + + conn = get(conn, "/api/v1/statuses/#{activity.id}/source") + + id = activity.id + + assert %{"id" => ^id, "text" => "mew mew #abc", "spoiler_text" => "#def"} = + json_response_and_validate_schema(conn, 200) + end + end + + describe "update status" do + setup do + oauth_access(["write:statuses"]) + end + + test "it updates the status" do + %{conn: conn, user: user} = oauth_access(["write:statuses", "read:statuses"]) + + {:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"}) + + conn + |> get("/api/v1/statuses/#{activity.id}") + |> json_response_and_validate_schema(200) + + response = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/statuses/#{activity.id}", %{ + "status" => "edited", + "spoiler_text" => "lol" + }) + |> json_response_and_validate_schema(200) + + assert response["content"] == "edited" + assert response["spoiler_text"] == "lol" + + response = + conn + |> get("/api/v1/statuses/#{activity.id}") + |> json_response_and_validate_schema(200) + + assert response["content"] == "edited" + assert response["spoiler_text"] == "lol" + end + + test "it updates the attachments", %{conn: conn, user: user} do + attachment = insert(:attachment, user: user) + attachment_id = to_string(attachment.id) + + {:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"}) + + response = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/statuses/#{activity.id}", %{ + "status" => "mew mew #abc", + "spoiler_text" => "#def", + "media_ids" => [attachment_id] + }) + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^attachment_id}] = response["media_attachments"] + end + + test "it does not update visibility", %{conn: conn, user: user} do + {:ok, activity} = + CommonAPI.post(user, %{ + status: "mew mew #abc", + spoiler_text: "#def", + visibility: "private" + }) + + response = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/statuses/#{activity.id}", %{ + "status" => "edited", + "spoiler_text" => "lol" + }) + |> json_response_and_validate_schema(200) + + assert response["visibility"] == "private" + end + + test "it refuses to update when original post is not by the user", %{conn: conn} do + another_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(another_user, %{status: "mew mew #abc", spoiler_text: "#def"}) + + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/statuses/#{activity.id}", %{ + "status" => "edited", + "spoiler_text" => "lol" + }) + |> json_response_and_validate_schema(:forbidden) + end + + test "it returns 404 if the user cannot see the post", %{conn: conn} do + another_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(another_user, %{ + status: "mew mew #abc", + spoiler_text: "#def", + visibility: "private" + }) + + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/statuses/#{activity.id}", %{ + "status" => "edited", + "spoiler_text" => "lol" + }) + |> json_response_and_validate_schema(:not_found) + end + end end diff --git a/test/pleroma/web/mastodon_api/controllers/subscription_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/subscription_controller_test.exs index ce7cfa9c7..837dc0dce 100644 --- a/test/pleroma/web/mastodon_api/controllers/subscription_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/subscription_controller_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do - use Pleroma.Web.ConnCase, async: true + use Pleroma.Web.ConnCase, async: false import Pleroma.Factory @@ -35,17 +35,20 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do defmacro assert_error_when_disable_push(do: yield) do quote do - vapid_details = Application.get_env(:web_push_encryption, :vapid_details, []) - Application.put_env(:web_push_encryption, :vapid_details, []) - assert %{"error" => "Web push subscription is disabled on this Pleroma instance"} == unquote(yield) - - Application.put_env(:web_push_encryption, :vapid_details, vapid_details) end end describe "when disabled" do + setup do + vapid_config = Application.get_env(:web_push_encryption, :vapid_details) + + Application.put_env(:web_push_encryption, :vapid_details, []) + + on_exit(fn -> Application.put_env(:web_push_encryption, :vapid_details, vapid_config) end) + end + test "POST returns error", %{conn: conn} do assert_error_when_disable_push do conn diff --git a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs index 2c7e78595..c120dd53c 100644 --- a/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs @@ -367,6 +367,47 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do } ] = result end + + test "should return local-only posts for authenticated users" do + user = insert(:user) + %{user: _reader, conn: conn} = oauth_access(["read:statuses"]) + + {:ok, %{id: id}} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"}) + + result = + conn + |> get("/api/v1/timelines/public") + |> json_response_and_validate_schema(200) + + assert [%{"id" => ^id}] = result + end + + test "should not return local-only posts for users without read:statuses" do + user = insert(:user) + %{user: _reader, conn: conn} = oauth_access([]) + + {:ok, _activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"}) + + result = + conn + |> get("/api/v1/timelines/public") + |> json_response_and_validate_schema(200) + + assert [] = result + end + + test "should not return local-only posts for anonymous users" do + user = insert(:user) + + {:ok, _activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"}) + + result = + build_conn() + |> get("/api/v1/timelines/public") + |> json_response_and_validate_schema(200) + + assert [] = result + end end defp local_and_remote_activities do @@ -486,7 +527,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do |> assign(:token, insert(:oauth_token, user: user_two, scopes: ["read:statuses"])) # Only direct should be visible here - res_conn = get(conn_user_two, "api/v1/timelines/direct") + res_conn = get(conn_user_two, "/api/v1/timelines/direct") assert [status] = json_response_and_validate_schema(res_conn, :ok) @@ -498,14 +539,14 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do build_conn() |> assign(:user, user_one) |> assign(:token, insert(:oauth_token, user: user_one, scopes: ["read:statuses"])) - |> get("api/v1/timelines/direct") + |> get("/api/v1/timelines/direct") [status] = json_response_and_validate_schema(res_conn, :ok) assert %{"visibility" => "direct"} = status # Both should be visible here - res_conn = get(conn_user_two, "api/v1/timelines/home") + res_conn = get(conn_user_two, "/api/v1/timelines/home") [_s1, _s2] = json_response_and_validate_schema(res_conn, :ok) @@ -518,14 +559,14 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do }) end) - res_conn = get(conn_user_two, "api/v1/timelines/direct") + res_conn = get(conn_user_two, "/api/v1/timelines/direct") statuses = json_response_and_validate_schema(res_conn, :ok) assert length(statuses) == 20 max_id = List.last(statuses)["id"] - res_conn = get(conn_user_two, "api/v1/timelines/direct?max_id=#{max_id}") + res_conn = get(conn_user_two, "/api/v1/timelines/direct?max_id=#{max_id}") assert [status] = json_response_and_validate_schema(res_conn, :ok) @@ -550,7 +591,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do visibility: "direct" }) - res_conn = get(conn, "api/v1/timelines/direct") + res_conn = get(conn, "/api/v1/timelines/direct") [status] = json_response_and_validate_schema(res_conn, :ok) assert status["id"] == direct.id @@ -903,7 +944,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do end end - describe "hashtag timeline handling of :restrict_unauthenticated setting" do + describe "hashtag timeline handling of restrict_unauthenticated setting" do setup do user = insert(:user) {:ok, activity1} = CommonAPI.post(user, %{status: "test #tag1"}) diff --git a/test/pleroma/web/mastodon_api/mastodon_api_test.exs b/test/pleroma/web/mastodon_api/mastodon_api_test.exs index 250a20352..190c13611 100644 --- a/test/pleroma/web/mastodon_api/mastodon_api_test.exs +++ b/test/pleroma/web/mastodon_api/mastodon_api_test.exs @@ -7,11 +7,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPITest do alias Pleroma.Notification alias Pleroma.ScheduledActivity + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.User alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.MastodonAPI import Pleroma.Factory + import Mox describe "follow/3" do test "returns error when followed user is deactivated" do @@ -88,6 +90,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPITest do describe "get_scheduled_activities/2" do test "returns user scheduled activities" do + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + user = insert(:user) today = diff --git a/test/pleroma/web/mastodon_api/update_credentials_test.exs b/test/pleroma/web/mastodon_api/update_credentials_test.exs index c2e4debc8..bea0cae69 100644 --- a/test/pleroma/web/mastodon_api/update_credentials_test.exs +++ b/test/pleroma/web/mastodon_api/update_credentials_test.exs @@ -4,13 +4,22 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do alias Pleroma.Repo + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.User use Pleroma.Web.ConnCase import Mock + import Mox import Pleroma.Factory + setup do + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + + :ok + end + describe "updating credentials" do setup do: oauth_access(["write:accounts"]) setup :request_content_type @@ -97,6 +106,42 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do assert user.raw_bio == raw_bio end + test "updating bio honours bio limit", %{conn: conn} do + bio_limit = Config.get([:instance, :user_bio_length], 5000) + + raw_bio = String.duplicate(".", bio_limit + 1) + + conn = patch(conn, "/api/v1/accounts/update_credentials", %{"note" => raw_bio}) + + assert %{"error" => "Bio is too long"} = json_response_and_validate_schema(conn, 413) + end + + test "updating name honours name limit", %{conn: conn} do + name_limit = Config.get([:instance, :user_name_length], 100) + + name = String.duplicate(".", name_limit + 1) + + conn = patch(conn, "/api/v1/accounts/update_credentials", %{"display_name" => name}) + + assert %{"error" => "Name is too long"} = json_response_and_validate_schema(conn, 413) + end + + test "when both name and bio exceeds the limit, display name error", %{conn: conn} do + name_limit = Config.get([:instance, :user_name_length], 100) + bio_limit = Config.get([:instance, :user_bio_length], 5000) + + name = String.duplicate(".", name_limit + 1) + raw_bio = String.duplicate(".", bio_limit + 1) + + conn = + patch(conn, "/api/v1/accounts/update_credentials", %{ + "display_name" => name, + "note" => raw_bio + }) + + assert %{"error" => "Name is too long"} = json_response_and_validate_schema(conn, 413) + end + test "updates the user's locking status", %{conn: conn} do conn = patch(conn, "/api/v1/accounts/update_credentials", %{locked: "true"}) @@ -259,6 +304,34 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do assert user.avatar == nil end + test "updates the user's avatar, upload_limit, returns a HTTP 413", %{conn: conn, user: user} do + upload_limit = Config.get([:instance, :upload_limit]) * 8 + 8 + + assert :ok == + File.write(Path.absname("test/tmp/large_binary.data"), <<0::size(upload_limit)>>) + + new_avatar_oversized = %Plug.Upload{ + content_type: nil, + path: Path.absname("test/tmp/large_binary.data"), + filename: "large_binary.data" + } + + assert user.avatar == %{} + + res = + patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => new_avatar_oversized}) + + assert user_response = json_response_and_validate_schema(res, 413) + assert user_response["avatar"] != User.avatar_url(user) + + user = User.get_by_id(user.id) + assert user.avatar == %{} + + clear_config([:instance, :upload_limit], upload_limit) + + assert :ok == File.rm(Path.absname("test/tmp/large_binary.data")) + end + test "updates the user's banner", %{user: user, conn: conn} do new_header = %Plug.Upload{ content_type: "image/jpeg", @@ -278,6 +351,32 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do assert user.banner == nil end + test "updates the user's banner, upload_limit, returns a HTTP 413", %{conn: conn, user: user} do + upload_limit = Config.get([:instance, :upload_limit]) * 8 + 8 + + assert :ok == + File.write(Path.absname("test/tmp/large_binary.data"), <<0::size(upload_limit)>>) + + new_header_oversized = %Plug.Upload{ + content_type: nil, + path: Path.absname("test/tmp/large_binary.data"), + filename: "large_binary.data" + } + + res = + patch(conn, "/api/v1/accounts/update_credentials", %{"header" => new_header_oversized}) + + assert user_response = json_response_and_validate_schema(res, 413) + assert user_response["header"] != User.banner_url(user) + + user = User.get_by_id(user.id) + assert user.banner == %{} + + clear_config([:instance, :upload_limit], upload_limit) + + assert :ok == File.rm(Path.absname("test/tmp/large_binary.data")) + end + test "updates the user's background", %{conn: conn, user: user} do new_header = %Plug.Upload{ content_type: "image/jpeg", @@ -301,6 +400,64 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do assert user.background == nil end + test "updates the user's background, upload_limit, returns a HTTP 413", %{ + conn: conn, + user: user + } do + upload_limit = Config.get([:instance, :upload_limit]) * 8 + 8 + + assert :ok == + File.write(Path.absname("test/tmp/large_binary.data"), <<0::size(upload_limit)>>) + + new_background_oversized = %Plug.Upload{ + content_type: nil, + path: Path.absname("test/tmp/large_binary.data"), + filename: "large_binary.data" + } + + res = + patch(conn, "/api/v1/accounts/update_credentials", %{ + "pleroma_background_image" => new_background_oversized + }) + + assert %{"error" => "File is too large"} == json_response_and_validate_schema(res, 413) + + user = Repo.get(User, user.id) + assert user.background == %{} + + clear_config([:instance, :upload_limit], upload_limit) + + assert :ok == File.rm(Path.absname("test/tmp/large_binary.data")) + end + + test "Strip / from upload files", %{user: user, conn: conn} do + new_image = %Plug.Upload{ + content_type: "image/jpeg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "../../../../nested/an_image.jpg" + } + + assert user.avatar == %{} + + res = + patch(conn, "/api/v1/accounts/update_credentials", %{ + "avatar" => new_image, + "header" => new_image, + "pleroma_background_image" => new_image + }) + + assert user_response = json_response_and_validate_schema(res, 200) + assert user_response["avatar"] + assert user_response["header"] + assert user_response["pleroma"]["background_image"] + refute Regex.match?(~r"/nested/", user_response["avatar"]) + refute Regex.match?(~r"/nested/", user_response["header"]) + refute Regex.match?(~r"/nested/", user_response["pleroma"]["background_image"]) + + user = User.get_by_id(user.id) + refute user.avatar == %{} + end + test "requires 'write:accounts' permission" do token1 = insert(:oauth_token, scopes: ["read"]) token2 = insert(:oauth_token, scopes: ["write", "follow"]) @@ -354,10 +511,15 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do |> json_response_and_validate_schema(200) assert account_data["fields"] == [ - %{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "bar"}, + %{ + "name" => "<a href=\"http://google.com\">foo</a>", + "value" => "bar", + "verified_at" => nil + }, %{ "name" => "link.io", - "value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>) + "value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>), + "verified_at" => nil } ] @@ -390,6 +552,20 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do assert user_data["source"]["pleroma"]["show_birthday"] == true end + test "unsets birth date", %{conn: conn} do + patch(conn, "/api/v1/accounts/update_credentials", %{ + "birthday" => "2001-02-12" + }) + + res = + patch(conn, "/api/v1/accounts/update_credentials", %{ + "birthday" => "" + }) + + assert user_data = json_response_and_validate_schema(res, 200) + assert user_data["pleroma"]["birthday"] == nil + end + test "emojis in fields labels", %{conn: conn} do fields = [ %{"name" => ":firefox:", "value" => "is best 2hu"}, @@ -402,8 +578,8 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do |> json_response_and_validate_schema(200) assert account_data["fields"] == [ - %{"name" => ":firefox:", "value" => "is best 2hu"}, - %{"name" => "they wins", "value" => ":blank:"} + %{"name" => ":firefox:", "value" => "is best 2hu", "verified_at" => nil}, + %{"name" => "they wins", "value" => ":blank:", "verified_at" => nil} ] assert account_data["source"]["fields"] == [ @@ -431,10 +607,11 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do |> json_response_and_validate_schema(200) assert account["fields"] == [ - %{"name" => "foo", "value" => "bar"}, + %{"name" => "foo", "value" => "bar", "verified_at" => nil}, %{ "name" => "link", - "value" => ~S(<a href="http://cofe.io" rel="ugc">http://cofe.io</a>) + "value" => ~S(<a href="http://cofe.io" rel="ugc">http://cofe.io</a>), + "verified_at" => nil } ] @@ -456,7 +633,7 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do |> json_response_and_validate_schema(200) assert account["fields"] == [ - %{"name" => "foo", "value" => ""} + %{"name" => "foo", "value" => "", "verified_at" => nil} ] end @@ -469,17 +646,17 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do fields = [%{"name" => "foo", "value" => long_value}] - assert %{"error" => "Invalid request"} == + assert %{"error" => "One or more field entries are too long"} == conn |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) - |> json_response_and_validate_schema(403) + |> json_response_and_validate_schema(413) fields = [%{"name" => long_name, "value" => "bar"}] - assert %{"error" => "Invalid request"} == + assert %{"error" => "One or more field entries are too long"} == conn |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) - |> json_response_and_validate_schema(403) + |> json_response_and_validate_schema(413) clear_config([:instance, :max_account_fields], 1) @@ -488,10 +665,10 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do %{"name" => "link", "value" => "cofe.io"} ] - assert %{"error" => "Invalid request"} == + assert %{"error" => "Too many field entries"} == conn |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) - |> json_response_and_validate_schema(403) + |> json_response_and_validate_schema(413) end end @@ -561,4 +738,20 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do assert account["source"]["pleroma"]["actor_type"] == "Person" end end + + describe "Mark account as group" do + setup do: oauth_access(["write:accounts"]) + setup :request_content_type + + test "changing actor_type to Group makes account a Group and enables bot indicator for backward compatibility", + %{conn: conn} do + account = + conn + |> patch("/api/v1/accounts/update_credentials", %{actor_type: "Group"}) + |> json_response_and_validate_schema(200) + + assert account["bot"] + assert account["source"]["pleroma"]["actor_type"] == "Group" + end + end end diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index 8ed37fe58..8dcdaf447 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -3,13 +3,15 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.MastodonAPI.AccountViewTest do - use Pleroma.DataCase + use Pleroma.DataCase, async: false + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.User alias Pleroma.UserRelationship alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.AccountView + import Mox import Pleroma.Factory import Tesla.Mock @@ -35,7 +37,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do inserted_at: ~N[2017-08-15 15:47:06.597036], emoji: %{"karjalanpiirakka" => "/file.png"}, raw_bio: "valid html. a\nb\nc\nd\nf '&<>\"", - also_known_as: ["https://shitposter.zone/users/shp"] + also_known_as: ["https://shitposter.zone/users/shp"], + last_status_at: NaiveDateTime.utc_now() }) expected = %{ @@ -74,7 +77,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do fields: [] }, fqn: "shp@shitposter.club", - last_status_at: nil, + last_status_at: user.last_status_at |> NaiveDateTime.to_date() |> Date.to_iso8601(), pleroma: %{ ap_id: user.ap_id, also_known_as: ["https://shitposter.zone/users/shp"], @@ -84,6 +87,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do tags: [], is_admin: false, is_moderator: false, + privileges: [], is_suggested: false, hide_favorites: true, hide_followers: false, @@ -99,6 +103,147 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true}) end + describe "roles and privileges" do + setup do + clear_config([:instance, :moderator_privileges], [:cofe, :only_moderator]) + clear_config([:instance, :admin_privileges], [:cofe, :only_admin]) + + %{ + user: insert(:user), + moderator: insert(:user, is_moderator: true), + admin: insert(:user, is_admin: true), + moderator_admin: insert(:user, is_moderator: true, is_admin: true), + user_no_show_roles: insert(:user, show_role: false), + moderator_admin_no_show_roles: + insert(:user, is_moderator: true, is_admin: true, show_role: false) + } + end + + test "shows roles and privileges when show_role: true", %{ + user: user, + moderator: moderator, + admin: admin, + moderator_admin: moderator_admin, + user_no_show_roles: user_no_show_roles, + moderator_admin_no_show_roles: moderator_admin_no_show_roles + } do + assert %{pleroma: %{is_moderator: false, is_admin: false}} = + AccountView.render("show.json", %{user: user, skip_visibility_check: true}) + + assert [] == + AccountView.render("show.json", %{user: user, skip_visibility_check: true})[ + :pleroma + ][:privileges] + |> Enum.sort() + + assert %{pleroma: %{is_moderator: true, is_admin: false}} = + AccountView.render("show.json", %{user: moderator, skip_visibility_check: true}) + + assert [:cofe, :only_moderator] == + AccountView.render("show.json", %{user: moderator, skip_visibility_check: true})[ + :pleroma + ][:privileges] + |> Enum.sort() + + assert %{pleroma: %{is_moderator: false, is_admin: true}} = + AccountView.render("show.json", %{user: admin, skip_visibility_check: true}) + + assert [:cofe, :only_admin] == + AccountView.render("show.json", %{user: admin, skip_visibility_check: true})[ + :pleroma + ][:privileges] + |> Enum.sort() + + assert %{pleroma: %{is_moderator: true, is_admin: true}} = + AccountView.render("show.json", %{ + user: moderator_admin, + skip_visibility_check: true + }) + + assert [:cofe, :only_admin, :only_moderator] == + AccountView.render("show.json", %{ + user: moderator_admin, + skip_visibility_check: true + })[:pleroma][:privileges] + |> Enum.sort() + + refute match?( + %{pleroma: %{is_moderator: _}}, + AccountView.render("show.json", %{ + user: user_no_show_roles, + skip_visibility_check: true + }) + ) + + refute match?( + %{pleroma: %{is_admin: _}}, + AccountView.render("show.json", %{ + user: user_no_show_roles, + skip_visibility_check: true + }) + ) + + refute match?( + %{pleroma: %{privileges: _}}, + AccountView.render("show.json", %{ + user: user_no_show_roles, + skip_visibility_check: true + }) + ) + + refute match?( + %{pleroma: %{is_moderator: _}}, + AccountView.render("show.json", %{ + user: moderator_admin_no_show_roles, + skip_visibility_check: true + }) + ) + + refute match?( + %{pleroma: %{is_admin: _}}, + AccountView.render("show.json", %{ + user: moderator_admin_no_show_roles, + skip_visibility_check: true + }) + ) + + refute match?( + %{pleroma: %{privileges: _}}, + AccountView.render("show.json", %{ + user: moderator_admin_no_show_roles, + skip_visibility_check: true + }) + ) + end + + test "shows roles and privileges when viewing own account, even when show_role: false", %{ + user_no_show_roles: user_no_show_roles, + moderator_admin_no_show_roles: moderator_admin_no_show_roles + } do + assert %{pleroma: %{is_moderator: false, is_admin: false, privileges: []}} = + AccountView.render("show.json", %{ + user: user_no_show_roles, + skip_visibility_check: true, + for: user_no_show_roles + }) + + assert %{ + pleroma: %{ + is_moderator: true, + is_admin: true, + privileges: privileges + } + } = + AccountView.render("show.json", %{ + user: moderator_admin_no_show_roles, + skip_visibility_check: true, + for: moderator_admin_no_show_roles + }) + + assert [:cofe, :only_admin, :only_moderator] == privileges |> Enum.sort() + end + end + describe "favicon" do setup do [user: insert(:user)] @@ -186,6 +331,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do tags: [], is_admin: false, is_moderator: false, + privileges: [], is_suggested: false, hide_favorites: true, hide_followers: false, @@ -214,8 +360,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do assert represented.url == "https://channels.tests.funkwhale.audio/channels/compositions" end - test "Represent a deactivated user for an admin" do - admin = insert(:user, is_admin: true) + test "Represent a deactivated user for a privileged user" do + clear_config([:instance, :moderator_privileges], [:users_manage_activation_state]) + + admin = insert(:user, is_moderator: true) deactivated_user = insert(:user, is_active: false) represented = AccountView.render("show.json", %{user: deactivated_user, for: admin}) assert represented[:pleroma][:deactivated] == true @@ -607,6 +755,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do clear_config([:media_proxy, :enabled], true) clear_config([:media_preview_proxy, :enabled]) + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + user = insert(:user, avatar: %{"url" => [%{"href" => "https://evil.website/avatar.png"}]}, @@ -614,7 +765,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do emoji: %{"joker_smile" => "https://evil.website/society.png"} ) - with media_preview_enabled <- [false, true] do + Enum.each([true, false], fn media_preview_enabled -> clear_config([:media_preview_proxy, :enabled], media_preview_enabled) AccountView.render("show.json", %{user: user, skip_visibility_check: true}) @@ -632,6 +783,23 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do true end) |> assert() - end + end) + end + + test "renders mute expiration date" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _user_relationships} = + User.mute(user, other_user, %{notifications: true, duration: 24 * 60 * 60}) + + %{ + mute_expires_at: mute_expires_at + } = AccountView.render("show.json", %{user: other_user, for: user, mutes: true}) + + assert DateTime.diff( + mute_expires_at, + DateTime.utc_now() |> DateTime.add(24 * 60 * 60) + ) in -3..3 end end diff --git a/test/pleroma/web/mastodon_api/views/notification_view_test.exs b/test/pleroma/web/mastodon_api/views/notification_view_test.exs index 8e4c9136a..9896f81b6 100644 --- a/test/pleroma/web/mastodon_api/views/notification_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/notification_view_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do - use Pleroma.DataCase + use Pleroma.DataCase, async: false alias Pleroma.Activity alias Pleroma.Chat @@ -22,6 +22,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView import Pleroma.Factory + setup do + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + :ok + end + defp test_notifications_rendering(notifications, user, expected_result) do result = NotificationView.render("index.json", %{notifications: notifications, for: user}) @@ -190,7 +195,47 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do emoji: "☕", account: AccountView.render("show.json", %{user: other_user, for: user}), status: StatusView.render("show.json", %{activity: activity, for: user}), - created_at: Utils.to_masto_date(notification.inserted_at) + created_at: Utils.to_masto_date(notification.inserted_at), + emoji_url: nil + } + + test_notifications_rendering([notification], user, [expected]) + end + + test "EmojiReact custom emoji notification" do + user = insert(:user) + other_user = insert(:user) + + note = + insert(:note, + user: user, + data: %{ + "reactions" => [ + ["👍", [user.ap_id], nil], + ["dinosaur", [user.ap_id], "http://localhost:4001/emoji/dino walking.gif"] + ] + } + ) + + activity = insert(:note_activity, note: note, user: user) + + {:ok, _activity} = CommonAPI.react_with_emoji(activity.id, other_user, "dinosaur") + + activity = Repo.get(Activity, activity.id) + + [notification] = Notification.for_user(user) + + assert notification + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false, is_muted: false}, + type: "pleroma:emoji_reaction", + emoji: ":dinosaur:", + account: AccountView.render("show.json", %{user: other_user, for: user}), + status: StatusView.render("show.json", %{activity: activity, for: user}), + created_at: Utils.to_masto_date(notification.inserted_at), + emoji_url: "http://localhost:4001/emoji/dino walking.gif" } test_notifications_rendering([notification], user, [expected]) @@ -218,9 +263,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do end test "Report notification" do + clear_config([:instance, :moderator_privileges], [:reports_manage_reports]) + reporting_user = insert(:user) reported_user = insert(:user) - {:ok, moderator_user} = insert(:user) |> User.admin_api_update(%{is_moderator: true}) + moderator_user = insert(:user, is_moderator: true) {:ok, activity} = CommonAPI.report(reporting_user, %{account_id: reported_user.id}) {:ok, [notification]} = Notification.create_notifications(activity) @@ -237,6 +284,32 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do test_notifications_rendering([notification], moderator_user, [expected]) end + test "Edit notification" do + user = insert(:user) + repeat_user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "mew"}) + {:ok, _} = CommonAPI.repeat(activity.id, repeat_user) + {:ok, update} = CommonAPI.update(user, activity, %{status: "mew mew"}) + + user = Pleroma.User.get_by_ap_id(user.ap_id) + activity = Pleroma.Activity.normalize(activity) + update = Pleroma.Activity.normalize(update) + + {:ok, [notification]} = Notification.create_notifications(update) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false, is_muted: false}, + type: "update", + account: AccountView.render("show.json", %{user: user, for: repeat_user}), + created_at: Utils.to_masto_date(notification.inserted_at), + status: StatusView.render("show.json", %{activity: activity, for: repeat_user}) + } + + test_notifications_rendering([notification], repeat_user, [expected]) + end + test "muted notification" do user = insert(:user) another_user = insert(:user) @@ -258,4 +331,31 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do test_notifications_rendering([notification], user, [expected]) end + + test "Subscribed status notification" do + user = insert(:user) + subscriber = insert(:user) + + User.subscribe(subscriber, user) + + {:ok, activity} = CommonAPI.post(user, %{status: "hi"}) + {:ok, [notification]} = Notification.create_notifications(activity) + + user = User.get_cached_by_id(user.id) + + expected = %{ + id: to_string(notification.id), + pleroma: %{is_seen: false, is_muted: false}, + type: "status", + account: + AccountView.render("show.json", %{ + user: user, + for: subscriber + }), + status: StatusView.render("show.json", %{activity: activity, for: subscriber}), + created_at: Utils.to_masto_date(notification.inserted_at) + } + + test_notifications_rendering([notification], subscriber, [expected]) + end end diff --git a/test/pleroma/web/mastodon_api/views/poll_view_test.exs b/test/pleroma/web/mastodon_api/views/poll_view_test.exs index a73d862fd..3aa73c224 100644 --- a/test/pleroma/web/mastodon_api/views/poll_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/poll_view_test.exs @@ -43,7 +43,8 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do %{title: "why are you even asking?", votes_count: 0} ], votes_count: 0, - voters_count: 0 + voters_count: 0, + pleroma: %{non_anonymous: false} } result = PollView.render("show.json", %{object: object}) @@ -165,4 +166,11 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do ] } = PollView.render("show.json", %{object: object}) end + + test "that poll is non anonymous" do + object = Object.normalize("https://friends.grishka.me/posts/54642", fetch: true) + result = PollView.render("show.json", %{object: object}) + + assert result[:pleroma][:non_anonymous] == true + end end diff --git a/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs b/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs index e5e510d33..30b38c6c5 100644 --- a/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs @@ -4,12 +4,16 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityViewTest do use Pleroma.DataCase, async: true + alias Pleroma.ScheduledActivity + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.ScheduledActivityView alias Pleroma.Web.MastodonAPI.StatusView + + import Mox import Pleroma.Factory test "A scheduled activity with a media attachment" do @@ -27,6 +31,9 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityViewTest do filename: "an_image.jpg" } + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) attrs = %{ @@ -48,7 +55,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityViewTest do id: to_string(scheduled_activity.id), media_attachments: %{media_ids: [upload.id]} - |> Utils.attachments_from_ids() + |> Utils.attachments_from_ids(user) |> Enum.map(&StatusView.render("attachment.json", %{attachment: &1})), params: %{ in_reply_to_id: to_string(activity.id), diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index 5d81c92b9..1c2d7f7fd 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -11,16 +11,20 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do alias Pleroma.HTML alias Pleroma.Object alias Pleroma.Repo + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.User alias Pleroma.UserRelationship alias Pleroma.Web.CommonAPI - alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.RichMedia.Card + require Bitwise + + import Mox + import OpenApiSpex.TestAssertions import Pleroma.Factory import Tesla.Mock - import OpenApiSpex.TestAssertions setup do mock(fn env -> apply(HttpRequestMock, :request, [env]) end) @@ -34,16 +38,26 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do {:ok, activity} = CommonAPI.post(user, %{status: "dae cofe??"}) {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "☕") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, ":dinosaur:") {:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵") {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, ":dinosaur:") + activity = Repo.get(Activity, activity.id) status = StatusView.render("show.json", activity: activity) assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) assert status[:pleroma][:emoji_reactions] == [ - %{name: "☕", count: 2, me: false}, - %{name: "🍵", count: 1, me: false} + %{name: "☕", count: 2, me: false, url: nil, account_ids: [other_user.id, user.id]}, + %{ + count: 2, + me: false, + name: "dinosaur", + url: "http://localhost:4001/emoji/dino walking.gif", + account_ids: [other_user.id, user.id] + }, + %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]} ] status = StatusView.render("show.json", activity: activity, for: user) @@ -51,8 +65,36 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) assert status[:pleroma][:emoji_reactions] == [ - %{name: "☕", count: 2, me: true}, - %{name: "🍵", count: 1, me: false} + %{name: "☕", count: 2, me: true, url: nil, account_ids: [other_user.id, user.id]}, + %{ + count: 2, + me: true, + name: "dinosaur", + url: "http://localhost:4001/emoji/dino walking.gif", + account_ids: [other_user.id, user.id] + }, + %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]} + ] + end + + test "works with legacy-formatted reactions" do + user = insert(:user) + other_user = insert(:user) + + note = + insert(:note, + user: user, + data: %{ + "reactions" => [["😿", [other_user.ap_id]]] + } + ) + + activity = insert(:note_activity, user: user, note: note) + + status = StatusView.render("show.json", activity: activity, for: user) + + assert status[:pleroma][:emoji_reactions] == [ + %{name: "😿", count: 1, me: false, url: nil, account_ids: [other_user.id]} ] end @@ -65,11 +107,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do |> Object.update_data(%{"reactions" => %{"☕" => [user.ap_id], "x" => 1}}) activity = Activity.get_by_id(activity.id) - status = StatusView.render("show.json", activity: activity, for: user) assert status[:pleroma][:emoji_reactions] == [ - %{name: "☕", count: 1, me: true} + %{name: "☕", count: 1, me: true, url: nil, account_ids: [user.id]} ] end @@ -89,7 +130,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do status = StatusView.render("show.json", activity: activity) assert status[:pleroma][:emoji_reactions] == [ - %{name: "☕", count: 1, me: false} + %{name: "☕", count: 1, me: false, url: nil, account_ids: [other_user.id]} ] status = StatusView.render("show.json", activity: activity, for: user) @@ -101,19 +142,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do status = StatusView.render("show.json", activity: activity) assert status[:pleroma][:emoji_reactions] == [ - %{name: "☕", count: 2, me: false} + %{ + name: "☕", + count: 2, + me: false, + url: nil, + account_ids: [third_user.id, other_user.id] + } ] status = StatusView.render("show.json", activity: activity, for: user) assert status[:pleroma][:emoji_reactions] == [ - %{name: "☕", count: 1, me: false} + %{name: "☕", count: 1, me: false, url: nil, account_ids: [third_user.id]} ] status = StatusView.render("show.json", activity: activity, for: other_user) assert status[:pleroma][:emoji_reactions] == [ - %{name: "☕", count: 1, me: true} + %{name: "☕", count: 1, me: true, url: nil, account_ids: [other_user.id]} ] end @@ -154,6 +201,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) end + @tag capture_log: true test "returns a temporary ap_id based user for activities missing db users" do user = insert(:user) @@ -226,7 +274,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do object_data = Object.normalize(note, fetch: false).data user = User.get_cached_by_ap_id(note.data["actor"]) - convo_id = Utils.context_to_conversation_id(object_data["context"]) + convo_id = :erlang.crc32(object_data["context"]) |> Bitwise.band(Bitwise.bnot(0x8000_0000)) status = StatusView.render("show.json", %{activity: note}) @@ -246,6 +294,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do content: HTML.filter_tags(object_data["content"]), text: nil, created_at: created_at, + edited_at: nil, reblogs_count: 0, replies_count: 0, favourites_count: 0, @@ -279,7 +328,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do pleroma: %{ local: true, conversation_id: convo_id, + context: object_data["context"], in_reply_to_account_acct: nil, + quote: nil, + quote_id: nil, + quote_url: nil, + quote_visible: false, content: %{"text/plain" => HTML.strip_tags(object_data["content"])}, spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])}, expires_at: nil, @@ -287,7 +341,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do thread_muted: false, emoji_reactions: [], parent_visible: false, - pinned_at: nil + pinned_at: nil, + quotes_count: 0, + bookmark_folder: nil } } @@ -376,6 +432,88 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do assert status.in_reply_to_id == to_string(note.id) end + test "a quote post" do + post = insert(:note_activity) + user = insert(:user) + + {:ok, quote_post} = CommonAPI.post(user, %{status: "he", quote_id: post.id}) + {:ok, quoted_quote_post} = CommonAPI.post(user, %{status: "yo", quote_id: quote_post.id}) + + status = StatusView.render("show.json", %{activity: quoted_quote_post}) + + assert status.pleroma.quote.id == to_string(quote_post.id) + assert status.pleroma.quote_id == to_string(quote_post.id) + assert status.pleroma.quote_url == Object.normalize(quote_post).data["id"] + assert status.pleroma.quote_visible + + # Quotes don't go more than one level deep + refute status.pleroma.quote.pleroma.quote + assert status.pleroma.quote.pleroma.quote_id == to_string(post.id) + assert status.pleroma.quote.pleroma.quote_url == Object.normalize(post).data["id"] + assert status.pleroma.quote.pleroma.quote_visible + + # In an index + [status] = StatusView.render("index.json", %{activities: [quoted_quote_post], as: :activity}) + + assert status.pleroma.quote.id == to_string(quote_post.id) + end + + test "quoted private post" do + user = insert(:user) + + # Insert a private post + private = insert(:followers_only_note_activity, user: user) + private_object = Object.normalize(private) + + # Create a public post quoting the private post + quote_private = + insert(:note_activity, note: insert(:note, data: %{"quoteUrl" => private_object.data["id"]})) + + status = StatusView.render("show.json", %{activity: quote_private}) + + # The quote isn't rendered + refute status.pleroma.quote + assert status.pleroma.quote_url == private_object.data["id"] + refute status.pleroma.quote_visible + + # After following the user, the quote is rendered + follower = insert(:user) + CommonAPI.follow(follower, user) + + status = StatusView.render("show.json", %{activity: quote_private, for: follower}) + assert status.pleroma.quote.id == to_string(private.id) + assert status.pleroma.quote_visible + end + + test "quoted direct message" do + # Insert a direct message + direct = insert(:direct_note_activity) + direct_object = Object.normalize(direct) + + # Create a public post quoting the direct message + quote_direct = + insert(:note_activity, note: insert(:note, data: %{"quoteUrl" => direct_object.data["id"]})) + + status = StatusView.render("show.json", %{activity: quote_direct}) + + # The quote isn't rendered + refute status.pleroma.quote + assert status.pleroma.quote_url == direct_object.data["id"] + refute status.pleroma.quote_visible + end + + test "repost of quote post" do + post = insert(:note_activity) + user = insert(:user) + + {:ok, quote_post} = CommonAPI.post(user, %{status: "he", quote_id: post.id}) + {:ok, repost} = CommonAPI.repeat(quote_post.id, user) + + [status] = StatusView.render("index.json", %{activities: [repost], as: :activity}) + + assert status.reblog.pleroma.quote.id == to_string(post.id) + end + test "contains mentions" do user = insert(:user) mentioned = insert(:user) @@ -595,56 +733,105 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do describe "rich media cards" do test "a rich media card without a site name renders correctly" do - page_url = "http://example.com" + page_url = "https://example.com" - card = %{ - url: page_url, - image: page_url <> "/example.jpg", - title: "Example website" - } + {:ok, card} = + Card.create(page_url, %{image: page_url <> "/example.jpg", title: "Example website"}) - %{provider_name: "example.com"} = - StatusView.render("card.json", %{page_url: page_url, rich_media: card}) + assert match?(%{provider_name: "example.com"}, StatusView.render("card.json", card)) end test "a rich media card without a site name or image renders correctly" do - page_url = "http://example.com" + page_url = "https://example.com" - card = %{ - url: page_url, - title: "Example website" + fields = %{ + "url" => page_url, + "title" => "Example website" } - %{provider_name: "example.com"} = - StatusView.render("card.json", %{page_url: page_url, rich_media: card}) + {:ok, card} = Card.create(page_url, fields) + + assert match?(%{provider_name: "example.com"}, StatusView.render("card.json", card)) end test "a rich media card without an image renders correctly" do - page_url = "http://example.com" + page_url = "https://example.com" + + fields = %{ + "url" => page_url, + "site_name" => "Example site name", + "title" => "Example website" + } + + {:ok, card} = Card.create(page_url, fields) + + assert match?(%{provider_name: "example.com"}, StatusView.render("card.json", card)) + end - card = %{ - url: page_url, - site_name: "Example site name", - title: "Example website" + test "a rich media card without descriptions returns the fields with empty strings" do + page_url = "https://example.com" + + fields = %{ + "url" => page_url, + "site_name" => "Example site name", + "title" => "Example website" } - %{provider_name: "example.com"} = - StatusView.render("card.json", %{page_url: page_url, rich_media: card}) + {:ok, card} = Card.create(page_url, fields) + + assert match?( + %{description: "", image_description: ""}, + StatusView.render("card.json", card) + ) end test "a rich media card with all relevant data renders correctly" do - page_url = "http://example.com" - - card = %{ - url: page_url, - site_name: "Example site name", - title: "Example website", - image: page_url <> "/example.jpg", - description: "Example description" + page_url = "https://example.com" + + fields = %{ + "url" => page_url, + "site_name" => "Example site name", + "title" => "Example website", + "image" => page_url <> "/example.jpg", + "description" => "Example description" } - %{provider_name: "example.com"} = - StatusView.render("card.json", %{page_url: page_url, rich_media: card}) + {:ok, card} = Card.create(page_url, fields) + + assert match?(%{provider_name: "example.com"}, StatusView.render("card.json", card)) + end + + test "a rich media card has all media proxied" do + clear_config([:media_proxy, :enabled], true) + clear_config([:media_preview_proxy, :enabled]) + + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + + page_url = "https://example.com" + + fields = %{ + "url" => page_url, + "site_name" => "Example site name", + "title" => "Example website", + "image" => page_url <> "/example.jpg", + "audio" => page_url <> "/example.ogg", + "video" => page_url <> "/example.mp4", + "description" => "Example description" + } + + {:ok, card} = Card.create(page_url, fields) + + %{ + provider_name: "example.com", + image: image, + pleroma: %{opengraph: og} + } = StatusView.render("card.json", card) + + assert String.match?(image, ~r/\/proxy\//) + assert String.match?(og["image"], ~r/\/proxy\//) + assert String.match?(og["audio"], ~r/\/proxy\//) + assert String.match?(og["video"], ~r/\/proxy\//) end end @@ -708,4 +895,55 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do status = StatusView.render("show.json", activity: visible, for: poster) assert status.pleroma.parent_visible end + + test "it shows edited_at" do + poster = insert(:user) + + {:ok, post} = CommonAPI.post(poster, %{status: "hey"}) + + status = StatusView.render("show.json", activity: post) + refute status.edited_at + + {:ok, _} = CommonAPI.update(poster, post, %{status: "mew mew"}) + edited = Pleroma.Activity.normalize(post) + + status = StatusView.render("show.json", activity: edited) + assert status.edited_at + end + + test "with a source object" do + note = + insert(:note, + data: %{"source" => %{"content" => "object source", "mediaType" => "text/markdown"}} + ) + + activity = insert(:note_activity, note: note) + + status = StatusView.render("show.json", activity: activity, with_source: true) + assert status.text == "object source" + end + + describe "source.json" do + test "with a source object, renders both source and content type" do + note = + insert(:note, + data: %{"source" => %{"content" => "object source", "mediaType" => "text/markdown"}} + ) + + activity = insert(:note_activity, note: note) + + status = StatusView.render("source.json", activity: activity) + assert status.text == "object source" + assert status.content_type == "text/markdown" + end + + test "with a source string, renders source and put text/plain as the content type" do + note = insert(:note, data: %{"source" => "string source"}) + activity = insert(:note_activity, note: note) + + status = StatusView.render("source.json", activity: activity) + assert status.text == "string source" + assert status.content_type == "text/plain" + end + end end diff --git a/test/pleroma/web/media_proxy/invalidation/script_test.exs b/test/pleroma/web/media_proxy/invalidation/script_test.exs index 39ef365f4..3e8fd751d 100644 --- a/test/pleroma/web/media_proxy/invalidation/script_test.exs +++ b/test/pleroma/web/media_proxy/invalidation/script_test.exs @@ -10,11 +10,14 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.ScriptTest do test "it logs error when script is not found" do assert capture_log(fn -> - assert Invalidation.Script.purge( - ["http://example.com/media/example.jpg"], - script_path: "./example" - ) == {:error, "%ErlangError{original: :enoent}"} - end) =~ "Error while cache purge: %ErlangError{original: :enoent}" + assert {:error, msg} = + Invalidation.Script.purge( + ["http://example.com/media/example.jpg"], + script_path: "./example" + ) + + assert msg =~ ~r/%ErlangError{original: :enoent(, reason: nil)?}/ + end) =~ ~r/Error while cache purge: %ErlangError{original: :enoent(, reason: nil)?}/ capture_log(fn -> assert Invalidation.Script.purge( diff --git a/test/pleroma/web/media_proxy/media_proxy_controller_test.exs b/test/pleroma/web/media_proxy/media_proxy_controller_test.exs index 5ace2eee9..f0c1dd640 100644 --- a/test/pleroma/web/media_proxy/media_proxy_controller_test.exs +++ b/test/pleroma/web/media_proxy/media_proxy_controller_test.exs @@ -6,10 +6,20 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do use Pleroma.Web.ConnCase import Mock + import Mox + alias Pleroma.ReverseProxy.ClientMock + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.Web.MediaProxy alias Plug.Conn + setup do + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + + :ok + end + describe "Media Proxy" do setup do clear_config([:media_proxy, :enabled], true) @@ -74,6 +84,20 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do assert %Conn{status: 404, resp_body: "Not Found"} = get(conn, url) end end + + test "it applies sandbox CSP to MediaProxy requests", %{conn: conn} do + media_url = "https://lain.com/image.png" + media_proxy_url = MediaProxy.encode_url(media_url) + + ClientMock + |> expect(:request, fn :get, ^media_url, _, _, _ -> + {:ok, 200, [{"content-type", "image/png"}]} + end) + + %Conn{resp_headers: headers} = get(conn, media_proxy_url) + + assert {"content-security-policy", "sandbox;"} in headers + end end describe "Media Preview Proxy" do @@ -158,7 +182,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do media_proxy_url: media_proxy_url } do Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> + %{method: :head, url: ^media_proxy_url} -> %Tesla.Env{status: 500, body: ""} end) @@ -173,7 +197,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do media_proxy_url: media_proxy_url } do Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> + %{method: :head, url: ^media_proxy_url} -> %Tesla.Env{status: 200, body: "", headers: [{"content-type", "application/pdf"}]} end) @@ -193,7 +217,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do clear_config([:media_preview_proxy, :min_content_length], 1_000_000_000) Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> + %{method: :head, url: ^media_proxy_url} -> %Tesla.Env{ status: 200, body: "", @@ -218,7 +242,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do media_proxy_url: media_proxy_url } do Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> + %{method: :head, url: ^media_proxy_url} -> %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/gif"}]} end) @@ -236,7 +260,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do media_proxy_url: media_proxy_url } do Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> + %{method: :head, url: ^media_proxy_url} -> %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]} end) @@ -256,7 +280,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do clear_config([:media_preview_proxy, :min_content_length], 100_000) Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> + %{method: :head, url: ^media_proxy_url} -> %Tesla.Env{ status: 200, body: "", @@ -278,7 +302,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do assert_dependencies_installed() Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> + %{method: :head, url: ^media_proxy_url} -> %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/png"}]} %{method: :get, url: ^media_proxy_url} -> @@ -300,7 +324,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do assert_dependencies_installed() Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> + %{method: :head, url: ^media_proxy_url} -> %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]} %{method: :get, url: ^media_proxy_url} -> @@ -320,7 +344,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do media_proxy_url: media_proxy_url } do Tesla.Mock.mock(fn - %{method: "head", url: ^media_proxy_url} -> + %{method: :head, url: ^media_proxy_url} -> %Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]} %{method: :get, url: ^media_proxy_url} -> diff --git a/test/pleroma/web/media_proxy_test.exs b/test/pleroma/web/media_proxy_test.exs index ffab1247f..718892665 100644 --- a/test/pleroma/web/media_proxy_test.exs +++ b/test/pleroma/web/media_proxy_test.exs @@ -7,9 +7,19 @@ defmodule Pleroma.Web.MediaProxyTest do use Pleroma.Tests.Helpers alias Pleroma.Config + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.Web.Endpoint alias Pleroma.Web.MediaProxy + import Mox + + setup do + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + + :ok + end + defp decode_result(encoded) do {:ok, decoded} = MediaProxy.decode_url(encoded) decoded @@ -222,7 +232,12 @@ defmodule Pleroma.Web.MediaProxyTest do test "ensure Pleroma.Upload base_url is always whitelisted" do media_url = "https://media.pleroma.social" - clear_config([Pleroma.Upload, :base_url], media_url) + + ConfigMock + |> stub(:get, fn + [Pleroma.Upload, :base_url] -> media_url + path -> Pleroma.Test.StaticConfig.get(path) + end) url = "#{media_url}/static/logo.png" encoded = MediaProxy.url(url) diff --git a/test/pleroma/web/metadata/providers/open_graph_test.exs b/test/pleroma/web/metadata/providers/open_graph_test.exs index b7ce95f7d..6a0fc9b10 100644 --- a/test/pleroma/web/metadata/providers/open_graph_test.exs +++ b/test/pleroma/web/metadata/providers/open_graph_test.exs @@ -4,9 +4,19 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraphTest do use Pleroma.DataCase + import Mox import Pleroma.Factory + + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.Web.Metadata.Providers.OpenGraph + setup do + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + + :ok + end + setup do: clear_config([Pleroma.Web.Metadata, :unfurl_nsfw]) test "it renders all supported types of attachments and skips unknown types" do diff --git a/test/pleroma/web/metadata/providers/rel_me_test.exs b/test/pleroma/web/metadata/providers/rel_me_test.exs index cce4f3607..793669037 100644 --- a/test/pleroma/web/metadata/providers/rel_me_test.exs +++ b/test/pleroma/web/metadata/providers/rel_me_test.exs @@ -11,11 +11,24 @@ defmodule Pleroma.Web.Metadata.Providers.RelMeTest do bio = ~s(<a href="https://some-link.com">https://some-link.com</a> <a rel="me" href="https://another-link.com">https://another-link.com</a> <link href="http://some.com"> <link rel="me" href="http://some3.com">) - user = insert(:user, %{bio: bio}) + fields = [ + %{ + "name" => "profile", + "value" => ~S(<a rel="me" href="http://profile.com">http://profile.com</a>) + }, + %{ + "name" => "like", + "value" => ~S(<a href="http://cofe.io">http://cofe.io</a>) + }, + %{"name" => "foo", "value" => "bar"} + ] + + user = insert(:user, %{bio: bio, fields: fields}) assert RelMe.build_tags(%{user: user}) == [ {:link, [rel: "me", href: "http://some3.com"], []}, - {:link, [rel: "me", href: "https://another-link.com"], []} + {:link, [rel: "me", href: "https://another-link.com"], []}, + {:link, [rel: "me", href: "http://profile.com"], []} ] end end diff --git a/test/pleroma/web/metadata/providers/twitter_card_test.exs b/test/pleroma/web/metadata/providers/twitter_card_test.exs index 392496993..f8d01c5c8 100644 --- a/test/pleroma/web/metadata/providers/twitter_card_test.exs +++ b/test/pleroma/web/metadata/providers/twitter_card_test.exs @@ -22,10 +22,10 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do res = TwitterCard.build_tags(%{user: user}) assert res == [ - {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, - {:meta, [property: "twitter:description", content: "born 19 March 1994"], []}, - {:meta, [property: "twitter:image", content: avatar_url], []}, - {:meta, [property: "twitter:card", content: "summary"], []} + {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []}, + {:meta, [name: "twitter:description", content: "born 19 March 1994"], []}, + {:meta, [name: "twitter:image", content: avatar_url], []}, + {:meta, [name: "twitter:card", content: "summary"], []} ] end @@ -39,6 +39,7 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do "actor" => user.ap_id, "tag" => [], "id" => "https://pleroma.gov/objects/whatever", + "summary" => "", "content" => "pleroma in a nutshell" } }) @@ -46,11 +47,41 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id}) assert [ - {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, - {:meta, [property: "twitter:description", content: "pleroma in a nutshell"], []}, - {:meta, [property: "twitter:image", content: "http://localhost:4001/images/avi.png"], + {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []}, + {:meta, [name: "twitter:description", content: "pleroma in a nutshell"], []}, + {:meta, [name: "twitter:image", content: "http://localhost:4001/images/avi.png"], []}, - {:meta, [property: "twitter:card", content: "summary"], []} + {:meta, [name: "twitter:card", content: "summary"], []} + ] == result + end + + test "it uses summary as description if post has one" do + user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") + {:ok, activity} = CommonAPI.post(user, %{status: "HI"}) + + note = + insert(:note, %{ + data: %{ + "actor" => user.ap_id, + "tag" => [], + "id" => "https://pleroma.gov/objects/whatever", + "summary" => "Public service announcement on caffeine consumption", + "content" => "cofe" + } + }) + + result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id}) + + assert [ + {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []}, + {:meta, + [ + name: "twitter:description", + content: "Public service announcement on caffeine consumption" + ], []}, + {:meta, [name: "twitter:image", content: "http://localhost:4001/images/avi.png"], + []}, + {:meta, [name: "twitter:card", content: "summary"], []} ] == result end @@ -65,6 +96,7 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do "actor" => user.ap_id, "tag" => [], "id" => "https://pleroma.gov/objects/whatever", + "summary" => "", "content" => "pleroma in a nutshell", "sensitive" => true, "attachment" => [ @@ -91,11 +123,11 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id}) assert [ - {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, - {:meta, [property: "twitter:description", content: "pleroma in a nutshell"], []}, - {:meta, [property: "twitter:image", content: "http://localhost:4001/images/avi.png"], + {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []}, + {:meta, [name: "twitter:description", content: "pleroma in a nutshell"], []}, + {:meta, [name: "twitter:image", content: "http://localhost:4001/images/avi.png"], []}, - {:meta, [property: "twitter:card", content: "summary"], []} + {:meta, [name: "twitter:card", content: "summary"], []} ] == result end @@ -109,6 +141,7 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do "actor" => user.ap_id, "tag" => [], "id" => "https://pleroma.gov/objects/whatever", + "summary" => "", "content" => "pleroma in a nutshell", "attachment" => [ %{ @@ -146,26 +179,27 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCardTest do result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id}) assert [ - {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, - {:meta, [property: "twitter:description", content: "pleroma in a nutshell"], []}, - {:meta, [property: "twitter:card", content: "summary_large_image"], []}, - {:meta, [property: "twitter:player", content: "https://pleroma.gov/tenshi.png"], []}, - {:meta, [property: "twitter:player:width", content: "1280"], []}, - {:meta, [property: "twitter:player:height", content: "1024"], []}, - {:meta, [property: "twitter:card", content: "player"], []}, + {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []}, + {:meta, [name: "twitter:description", content: "pleroma in a nutshell"], []}, + {:meta, [name: "twitter:card", content: "summary_large_image"], []}, + {:meta, [name: "twitter:image", content: "https://pleroma.gov/tenshi.png"], []}, + {:meta, [name: "twitter:image:alt", content: ""], []}, + {:meta, [name: "twitter:player:width", content: "1280"], []}, + {:meta, [name: "twitter:player:height", content: "1024"], []}, + {:meta, [name: "twitter:card", content: "player"], []}, {:meta, [ - property: "twitter:player", + name: "twitter:player", content: Router.Helpers.o_status_url(Endpoint, :notice_player, activity.id) ], []}, - {:meta, [property: "twitter:player:width", content: "800"], []}, - {:meta, [property: "twitter:player:height", content: "600"], []}, + {:meta, [name: "twitter:player:width", content: "800"], []}, + {:meta, [name: "twitter:player:height", content: "600"], []}, {:meta, [ - property: "twitter:player:stream", + name: "twitter:player:stream", content: "https://pleroma.gov/about/juche.webm" ], []}, - {:meta, [property: "twitter:player:stream:content_type", content: "video/webm"], []} + {:meta, [name: "twitter:player:stream:content_type", content: "video/webm"], []} ] == result end end diff --git a/test/pleroma/web/metadata/utils_test.exs b/test/pleroma/web/metadata/utils_test.exs index ce8ed5683..3daf852fb 100644 --- a/test/pleroma/web/metadata/utils_test.exs +++ b/test/pleroma/web/metadata/utils_test.exs @@ -3,12 +3,12 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Metadata.UtilsTest do - use Pleroma.DataCase, async: true + use Pleroma.DataCase, async: false import Pleroma.Factory alias Pleroma.Web.Metadata.Utils describe "scrub_html_and_truncate/1" do - test "it returns text without encode HTML" do + test "it returns content text without encode HTML if summary is nil" do user = insert(:user) note = @@ -16,15 +16,63 @@ defmodule Pleroma.Web.Metadata.UtilsTest do data: %{ "actor" => user.ap_id, "id" => "https://pleroma.gov/objects/whatever", + "summary" => nil, "content" => "Pleroma's really cool!" } }) assert Utils.scrub_html_and_truncate(note) == "Pleroma's really cool!" end + + test "it returns context text without encode HTML if summary is empty" do + user = insert(:user) + + note = + insert(:note, %{ + data: %{ + "actor" => user.ap_id, + "id" => "https://pleroma.gov/objects/whatever", + "summary" => "", + "content" => "Pleroma's really cool!" + } + }) + + assert Utils.scrub_html_and_truncate(note) == "Pleroma's really cool!" + end + + test "it returns summary text without encode HTML if summary is filled" do + user = insert(:user) + + note = + insert(:note, %{ + data: %{ + "actor" => user.ap_id, + "id" => "https://pleroma.gov/objects/whatever", + "summary" => "Public service announcement on caffeine consumption", + "content" => "cofe" + } + }) + + assert Utils.scrub_html_and_truncate(note) == + "Public service announcement on caffeine consumption" + end + + test "it does not return old content after editing" do + user = insert(:user) + + {:ok, activity} = Pleroma.Web.CommonAPI.post(user, %{status: "mew mew #def"}) + + object = Pleroma.Object.normalize(activity) + assert Utils.scrub_html_and_truncate(object) == "mew mew #def" + + {:ok, update} = Pleroma.Web.CommonAPI.update(user, activity, %{status: "mew mew #abc"}) + update = Pleroma.Activity.normalize(update) + object = Pleroma.Object.normalize(update) + assert Utils.scrub_html_and_truncate(object) == "mew mew #abc" + end end - describe "scrub_html_and_truncate/2" do + describe "scrub_html_and_truncate/3" do test "it returns text without encode HTML" do assert Utils.scrub_html_and_truncate("Pleroma's really cool!") == "Pleroma's really cool!" end diff --git a/test/pleroma/web/node_info_test.exs b/test/pleroma/web/node_info_test.exs index 247ad7501..f474220be 100644 --- a/test/pleroma/web/node_info_test.exs +++ b/test/pleroma/web/node_info_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.NodeInfoTest do - use Pleroma.Web.ConnCase + use Pleroma.Web.ConnCase, async: false import Pleroma.Factory @@ -40,6 +40,19 @@ defmodule Pleroma.Web.NodeInfoTest do assert admin.ap_id in result["metadata"]["staffAccounts"] end + test "nodeinfo shows roles and privileges", %{conn: conn} do + clear_config([:instance, :moderator_privileges], [:cofe]) + clear_config([:instance, :admin_privileges], [:suya, :cofe]) + + conn = + conn + |> get("/nodeinfo/2.1.json") + + assert result = json_response(conn, 200) + + assert %{"admin" => ["suya", "cofe"], "moderator" => ["cofe"]} == result["metadata"]["roles"] + end + test "nodeinfo shows restricted nicknames", %{conn: conn} do conn = conn diff --git a/test/pleroma/web/o_auth/mfa_controller_test.exs b/test/pleroma/web/o_auth/mfa_controller_test.exs index 62404c768..ac854e818 100644 --- a/test/pleroma/web/o_auth/mfa_controller_test.exs +++ b/test/pleroma/web/o_auth/mfa_controller_test.exs @@ -214,7 +214,7 @@ defmodule Pleroma.Web.OAuth.MFAControllerTest do assert response == %{"error" => "Invalid code"} end - test "returns error when client credentails is wrong ", %{conn: conn, user: user} do + test "returns error when client credentials is wrong ", %{conn: conn, user: user} do otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret) mfa_token = insert(:mfa_token, user: user) diff --git a/test/pleroma/web/o_auth/o_auth_controller_test.exs b/test/pleroma/web/o_auth/o_auth_controller_test.exs index f41d6a322..83a08d9fc 100644 --- a/test/pleroma/web/o_auth/o_auth_controller_test.exs +++ b/test/pleroma/web/o_auth/o_auth_controller_test.exs @@ -186,7 +186,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do assert html_response(conn, 302) assert redirected_to(conn) == app.redirect_uris - assert get_flash(conn, :error) == "Failed to authenticate: (error description)." + assert conn.assigns.flash["error"] == "Failed to authenticate: (error description)." end test "GET /oauth/registration_details renders registration details form", %{ @@ -307,7 +307,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do |> post("/oauth/register", bad_params) assert html_response(conn, 403) =~ ~r/name="op" type="submit" value="register"/ - assert get_flash(conn, :error) == "Error: #{bad_param} has already been taken." + assert conn.assigns.flash["error"] == "Error: #{bad_param} has already been taken." end end @@ -398,7 +398,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do |> post("/oauth/register", params) assert html_response(conn, 401) =~ ~r/name="op" type="submit" value="connect"/ - assert get_flash(conn, :error) == "Invalid Username/Password" + assert conn.assigns.flash["error"] == "Invalid Username/Password" end end diff --git a/test/pleroma/web/o_auth/token/utils_test.exs b/test/pleroma/web/o_auth/token/utils_test.exs index e688ad750..f4027985d 100644 --- a/test/pleroma/web/o_auth/token/utils_test.exs +++ b/test/pleroma/web/o_auth/token/utils_test.exs @@ -13,7 +13,7 @@ defmodule Pleroma.Web.OAuth.Token.UtilsTest do Utils.fetch_app(%Plug.Conn{params: %{"client_id" => 1, "client_secret" => "x"}}) end - test "returns App by params credentails" do + test "returns App by params credentials" do app = insert(:oauth_app) assert {:ok, load_app} = @@ -24,7 +24,7 @@ defmodule Pleroma.Web.OAuth.Token.UtilsTest do assert load_app == app end - test "returns App by header credentails" do + test "returns App by header credentials" do app = insert(:oauth_app) header = "Basic " <> Base.encode64("#{app.client_id}:#{app.client_secret}") diff --git a/test/pleroma/web/o_status/o_status_controller_test.exs b/test/pleroma/web/o_status/o_status_controller_test.exs index 5abbcfbdc..3e8fcd956 100644 --- a/test/pleroma/web/o_status/o_status_controller_test.exs +++ b/test/pleroma/web/o_status/o_status_controller_test.exs @@ -196,7 +196,7 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do |> get("/notice/#{like_activity.id}") |> response(200) - assert resp =~ "<!--server-generated-meta-->" + refute resp =~ ~r(<meta content="[^"]*" property="og:url") end test "404s a private notice", %{conn: conn} do @@ -343,54 +343,4 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do |> response(200) end end - - describe "notice compatibility routes" do - test "Soapbox FE", %{conn: conn} do - user = insert(:user) - note_activity = insert(:note_activity, user: user) - - resp = - conn - |> put_req_header("accept", "text/html") - |> get("/@#{user.nickname}/posts/#{note_activity.id}") - |> response(200) - - expected = - "<meta content=\"#{Endpoint.url()}/notice/#{note_activity.id}\" property=\"og:url\">" - - assert resp =~ expected - end - - test "Mastodon", %{conn: conn} do - user = insert(:user) - note_activity = insert(:note_activity, user: user) - - resp = - conn - |> put_req_header("accept", "text/html") - |> get("/@#{user.nickname}/#{note_activity.id}") - |> response(200) - - expected = - "<meta content=\"#{Endpoint.url()}/notice/#{note_activity.id}\" property=\"og:url\">" - - assert resp =~ expected - end - - test "Twitter", %{conn: conn} do - user = insert(:user) - note_activity = insert(:note_activity, user: user) - - resp = - conn - |> put_req_header("accept", "text/html") - |> get("/#{user.nickname}/status/#{note_activity.id}") - |> response(200) - - expected = - "<meta content=\"#{Endpoint.url()}/notice/#{note_activity.id}\" property=\"og:url\">" - - assert resp =~ expected - end - end end diff --git a/test/pleroma/web/pleroma_api/controllers/backup_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/backup_controller_test.exs index 650f3d80d..21e619fa4 100644 --- a/test/pleroma/web/pleroma_api/controllers/backup_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/backup_controller_test.exs @@ -5,13 +5,18 @@ defmodule Pleroma.Web.PleromaAPI.BackupControllerTest do use Pleroma.Web.ConnCase + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.User.Backup alias Pleroma.Web.PleromaAPI.BackupView setup do clear_config([Pleroma.Upload, :uploader]) clear_config([Backup, :limit_days]) - oauth_access(["read:accounts"]) + + ConfigMock + |> Mox.stub_with(Pleroma.Config) + + oauth_access(["read:backups"]) end test "GET /api/v1/pleroma/backups", %{user: user, conn: conn} do @@ -82,4 +87,24 @@ defmodule Pleroma.Web.PleromaAPI.BackupControllerTest do |> post("/api/v1/pleroma/backups") |> json_response_and_validate_schema(400) end + + test "Backup without email address" do + user = Pleroma.Factory.insert(:user, email: nil) + %{conn: conn} = oauth_access(["read:backups"], user: user) + + assert is_nil(user.email) + + assert [ + %{ + "content_type" => "application/zip", + "url" => _url, + "file_size" => 0, + "processed" => false, + "inserted_at" => _ + } + ] = + conn + |> post("/api/v1/pleroma/backups") + |> json_response_and_validate_schema(:ok) + end end diff --git a/test/pleroma/web/pleroma_api/controllers/bookmark_folder_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/bookmark_folder_controller_test.exs new file mode 100644 index 000000000..9bd90ed2e --- /dev/null +++ b/test/pleroma/web/pleroma_api/controllers/bookmark_folder_controller_test.exs @@ -0,0 +1,161 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only +defmodule Pleroma.Web.PleromaAPI.BookmarkFolderControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.BookmarkFolder + # alias Pleroma.Object + # alias Pleroma.Tests.Helpers + # alias Pleroma.UnstubbedConfigMock, as: ConfigMock + # alias Pleroma.User + # alias Pleroma.Web.ActivityPub.ActivityPub + # alias Pleroma.Web.CommonAPI + + # import Mox + import Pleroma.Factory + + describe "GET /api/v1/pleroma/bookmark_folders" do + setup do: oauth_access(["read:bookmarks"]) + + test "it lists bookmark folders", %{conn: conn, user: user} do + {:ok, folder} = BookmarkFolder.create(user.id, "Bookmark folder") + + folder_id = folder.id + + result = + conn + |> get("/api/v1/pleroma/bookmark_folders") + |> json_response_and_validate_schema(200) + + assert [ + %{ + "id" => ^folder_id, + "name" => "Bookmark folder", + "emoji" => nil, + "emoji_url" => nil + } + ] = result + end + end + + describe "POST /api/v1/pleroma/bookmark_folders" do + setup do: oauth_access(["write:bookmarks"]) + + test "it creates a bookmark folder", %{conn: conn} do + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/bookmark_folders", %{ + name: "Bookmark folder", + emoji: "📁" + }) + |> json_response_and_validate_schema(200) + + assert %{ + "name" => "Bookmark folder", + "emoji" => "📁", + "emoji_url" => nil + } = result + end + + test "it creates a bookmark folder with custom emoji", %{conn: conn} do + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/bookmark_folders", %{ + name: "Bookmark folder", + emoji: ":firefox:" + }) + |> json_response_and_validate_schema(200) + + assert %{ + "name" => "Bookmark folder", + "emoji" => ":firefox:", + "emoji_url" => "http://localhost:4001/emoji/Firefox.gif" + } = result + end + + test "it returns error for invalid emoji", %{conn: conn} do + result = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/bookmark_folders", %{ + name: "Bookmark folder", + emoji: "not an emoji" + }) + |> json_response_and_validate_schema(422) + + assert %{"error" => "Invalid emoji"} = result + end + end + + describe "PATCH /api/v1/pleroma/bookmark_folders/:id" do + setup do: oauth_access(["write:bookmarks"]) + + test "it updates a bookmark folder", %{conn: conn, user: user} do + {:ok, folder} = BookmarkFolder.create(user.id, "Bookmark folder") + + result = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/v1/pleroma/bookmark_folders/#{folder.id}", %{ + name: "bookmark folder" + }) + |> json_response_and_validate_schema(200) + + assert %{ + "name" => "bookmark folder" + } = result + end + + test "it returns error when updating others' folders", %{conn: conn} do + other_user = insert(:user) + + {:ok, folder} = BookmarkFolder.create(other_user.id, "Bookmark folder") + + result = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/v1/pleroma/bookmark_folders/#{folder.id}", %{ + name: "bookmark folder" + }) + |> json_response_and_validate_schema(403) + + assert %{ + "error" => "Access denied" + } = result + end + end + + describe "DELETE /api/v1/pleroma/bookmark_folders/:id" do + setup do: oauth_access(["write:bookmarks"]) + + test "it deleting a bookmark folder", %{conn: conn, user: user} do + {:ok, folder} = BookmarkFolder.create(user.id, "Bookmark folder") + + assert conn + |> delete("/api/v1/pleroma/bookmark_folders/#{folder.id}") + |> json_response_and_validate_schema(200) + + folders = BookmarkFolder.for_user(user.id) + + assert length(folders) == 0 + end + + test "it returns error when deleting others' folders", %{conn: conn} do + other_user = insert(:user) + + {:ok, folder} = BookmarkFolder.create(other_user.id, "Bookmark folder") + + result = + conn + |> patch("/api/v1/pleroma/bookmark_folders/#{folder.id}") + |> json_response_and_validate_schema(403) + + assert %{ + "error" => "Access denied" + } = result + end + end +end diff --git a/test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs index aa40c6f44..0d3452559 100644 --- a/test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs @@ -7,10 +7,13 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do alias Pleroma.Chat alias Pleroma.Chat.MessageReference alias Pleroma.Object + alias Pleroma.Tests.Helpers + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI + import Mox import Pleroma.Factory describe "POST /api/v1/pleroma/chats/:id/messages/:message_id/read" do @@ -112,6 +115,9 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do filename: "an_image.jpg" } + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) other_user = insert(:user) @@ -207,36 +213,63 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do result = json_response_and_validate_schema(response, 200) [next, prev] = get_resp_header(response, "link") |> hd() |> String.split(", ") - api_endpoint = "/api/v1/pleroma/chats/" + api_endpoint = Pleroma.Web.Endpoint.url() <> "/api/v1/pleroma/chats/" + + [next_url, next_rel] = String.split(next, ";") + next_url = String.trim_trailing(next_url, ">") |> String.trim_leading("<") + + next_url_sorted = Helpers.uri_query_sort(next_url) assert String.match?( - next, - ~r(#{api_endpoint}.*/messages\?limit=\d+&max_id=.*; rel=\"next\"$) + next_url_sorted, + ~r(#{api_endpoint}.*/messages\?limit=\d+&max_id=.*&offset=\d+$) ) + assert next_rel =~ "next" + + [prev_url, prev_rel] = String.split(prev, ";") + prev_url = String.trim_trailing(prev_url, ">") |> String.trim_leading("<") + + prev_url_sorted = Helpers.uri_query_sort(prev_url) + assert String.match?( - prev, - ~r(#{api_endpoint}.*/messages\?limit=\d+&min_id=.*; rel=\"prev\"$) + prev_url_sorted, + ~r(#{api_endpoint}.*/messages\?limit=\d+&min_id=.*&offset=\d+$) ) + assert prev_rel =~ "prev" + assert length(result) == 20 - response = - get(conn, "/api/v1/pleroma/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}") + response = get(conn, "#{api_endpoint}#{chat.id}/messages?max_id=#{List.last(result)["id"]}") result = json_response_and_validate_schema(response, 200) [next, prev] = get_resp_header(response, "link") |> hd() |> String.split(", ") + [next_url, next_rel] = String.split(next, ";") + next_url = String.trim_trailing(next_url, ">") |> String.trim_leading("<") + + next_url_sorted = Helpers.uri_query_sort(next_url) + assert String.match?( - next, - ~r(#{api_endpoint}.*/messages\?limit=\d+&max_id=.*; rel=\"next\"$) + next_url_sorted, + ~r(#{api_endpoint}.*/messages\?limit=\d+&max_id=.*&offset=\d+$) ) + assert next_rel =~ "next" + + [prev_url, prev_rel] = String.split(prev, ";") + prev_url = String.trim_trailing(prev_url, ">") |> String.trim_leading("<") + + prev_url_sorted = Helpers.uri_query_sort(prev_url) + assert String.match?( - prev, - ~r(#{api_endpoint}.*/messages\?limit=\d+&max_id=.*&min_id=.*; rel=\"prev\"$) + prev_url_sorted, + ~r(#{api_endpoint}.*/messages\?limit=\d+&max_id=.*&min_id=.*&offset=\d+$) ) + assert prev_rel =~ "prev" + assert length(result) == 10 end diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_file_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_file_controller_test.exs index 200ce3b68..540b452c7 100644 --- a/test/pleroma/web/pleroma_api/controllers/emoji_file_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/emoji_file_controller_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.PleromaAPI.EmojiFileControllerTest do - use Pleroma.Web.ConnCase + use Pleroma.Web.ConnCase, async: false import Mock import Tesla.Mock @@ -30,6 +30,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiFileControllerTest do describe "POST/PATCH/DELETE /api/pleroma/emoji/packs/files?name=:name" do setup do + clear_config([:instance, :admin_privileges], [:emoji_manage_emoji]) pack_file = "#{@emoji_path}/test_pack/pack.json" original_content = File.read!(pack_file) @@ -377,5 +378,32 @@ defmodule Pleroma.Web.PleromaAPI.EmojiFileControllerTest do }) |> json_response_and_validate_schema(:bad_request) end + + test "it requires privileged role :emoji_manage_emoji", %{admin_conn: admin_conn} do + clear_config([:instance, :admin_privileges], []) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/files?name=test_pack", %{ + file: %Plug.Upload{ + filename: "shortcode.png", + path: "#{Pleroma.Config.get([:instance, :static_dir])}/add/shortcode.png" + } + }) + |> json_response(:forbidden) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/files?name=test_pack", %{ + shortcode: "blank", + new_filename: "dir_2/blank_3.png" + }) + |> json_response(:forbidden) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> delete("/api/pleroma/emoji/packs/files?name=test_pack&shortcode=blank3") + |> json_response(:forbidden) + end end end diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs index d1fd1cbb0..92334487c 100644 --- a/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs @@ -99,6 +99,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do end describe "GET /api/pleroma/emoji/packs/remote" do + setup do + clear_config([:instance, :admin_privileges], [:emoji_manage_emoji]) + end + test "shareable instance", %{admin_conn: admin_conn, conn: conn} do resp = conn @@ -112,7 +116,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> json(%{metadata: %{features: ["shareable_emoji_packs"]}}) - %{method: :get, url: "https://example.com/api/pleroma/emoji/packs?page=2&page_size=1"} -> + %{method: :get, url: "https://example.com/api/v1/pleroma/emoji/packs?page=2&page_size=1"} -> json(resp) end) @@ -136,6 +140,14 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do "error" => "The requested instance does not support sharing emoji packs" } end + + test "it requires privileged role :emoji_manage_emoji", %{admin_conn: admin_conn} do + clear_config([:instance, :admin_privileges], []) + + assert admin_conn + |> get("/api/pleroma/emoji/packs/remote?url=https://example.com") + |> json_response(:forbidden) + end end describe "GET /api/pleroma/emoji/packs/archive?name=:name" do @@ -170,6 +182,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do end describe "POST /api/pleroma/emoji/packs/download" do + setup do + clear_config([:instance, :admin_privileges], [:emoji_manage_emoji]) + end + test "shared pack from remote and non shared from fallback-src", %{ admin_conn: admin_conn, conn: conn @@ -183,7 +199,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do %{ method: :get, - url: "https://example.com/api/pleroma/emoji/pack?name=test_pack" + url: "https://example.com/api/v1/pleroma/emoji/pack?name=test_pack&page_size=" <> _n } -> conn |> get("/api/pleroma/emoji/pack?name=test_pack") @@ -192,7 +208,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do %{ method: :get, - url: "https://example.com/api/pleroma/emoji/packs/archive?name=test_pack" + url: "https://example.com/api/v1/pleroma/emoji/packs/archive?name=test_pack" } -> conn |> get("/api/pleroma/emoji/packs/archive?name=test_pack") @@ -201,7 +217,9 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do %{ method: :get, - url: "https://example.com/api/pleroma/emoji/pack?name=test_pack_nonshared" + url: + "https://example.com/api/v1/pleroma/emoji/pack?name=test_pack_nonshared&page_size=" <> + _n } -> conn |> get("/api/pleroma/emoji/pack?name=test_pack_nonshared") @@ -289,14 +307,14 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do %{ method: :get, - url: "https://example.com/api/pleroma/emoji/pack?name=pack_bad_sha" + url: "https://example.com/api/v1/pleroma/emoji/pack?name=pack_bad_sha&page_size=" <> _n } -> {:ok, pack} = Pleroma.Emoji.Pack.load_pack("pack_bad_sha") %Tesla.Env{status: 200, body: Jason.encode!(pack)} %{ method: :get, - url: "https://example.com/api/pleroma/emoji/packs/archive?name=pack_bad_sha" + url: "https://example.com/api/v1/pleroma/emoji/packs/archive?name=pack_bad_sha" } -> %Tesla.Env{ status: 200, @@ -326,7 +344,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do %{ method: :get, - url: "https://example.com/api/pleroma/emoji/pack?name=test_pack" + url: "https://example.com/api/v1/pleroma/emoji/pack?name=test_pack&page_size=" <> _n } -> {:ok, pack} = Pleroma.Emoji.Pack.load_pack("test_pack") %Tesla.Env{status: 200, body: Jason.encode!(pack)} @@ -344,10 +362,24 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do "The pack was not set as shared and there is no fallback src to download from" } end + + test "it requires privileged role :emoji_manage_emoji", %{admin_conn: conn} do + clear_config([:instance, :admin_privileges], []) + + assert conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/download", %{ + url: "https://example.com", + name: "test_pack", + as: "test_pack2" + }) + |> json_response(:forbidden) + end end describe "PATCH/update /api/pleroma/emoji/pack?name=:name" do setup do + clear_config([:instance, :admin_privileges], [:emoji_manage_emoji]) pack_file = "#{@emoji_path}/test_pack/pack.json" original_content = File.read!(pack_file) @@ -435,9 +467,25 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do "error" => "The fallback archive does not have all files specified in pack.json" } end + + test "it requires privileged role :emoji_manage_emoji", %{ + admin_conn: conn, + new_data: new_data + } do + clear_config([:instance, :admin_privileges], []) + + assert conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/pack?name=test_pack", %{metadata: new_data}) + |> json_response(:forbidden) + end end describe "POST/DELETE /api/pleroma/emoji/pack?name=:name" do + setup do + clear_config([:instance, :admin_privileges], [:emoji_manage_emoji]) + end + test "returns an error on creates pack when file system not writable", %{ admin_conn: admin_conn } do @@ -520,6 +568,18 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do "error" => "pack name cannot be empty" } end + + test "it requires privileged role :emoji_manage_emoji", %{admin_conn: admin_conn} do + clear_config([:instance, :admin_privileges], []) + + assert admin_conn + |> post("/api/pleroma/emoji/pack?name= ") + |> json_response(:forbidden) + + assert admin_conn + |> delete("/api/pleroma/emoji/pack?name= ") + |> json_response(:forbidden) + end end test "deleting nonexisting pack", %{admin_conn: admin_conn} do @@ -578,6 +638,12 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do "blank2" => "blank.png", "foo" => "blank.png" } + + clear_config([:instance, :admin_privileges], []) + + assert admin_conn + |> get("/api/pleroma/emoji/packs/import") + |> json_response(:forbidden) end describe "GET /api/pleroma/emoji/pack?name=:name" do diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs index 77c75b560..8c2dcc1bb 100644 --- a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs @@ -13,27 +13,122 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do import Pleroma.Factory + setup do + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + :ok + end + test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do user = insert(:user) other_user = insert(:user) + note = insert(:note, user: user, data: %{"reactions" => [["👍", [other_user.ap_id], nil]]}) + activity = insert(:note_activity, note: note, user: user) + + result = + conn + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) + |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/\u26A0") + |> json_response_and_validate_schema(200) + + assert %{"id" => id} = result + assert to_string(activity.id) == id + + assert result["pleroma"]["emoji_reactions"] == [ + %{ + "name" => "👍", + "count" => 1, + "me" => true, + "url" => nil, + "account_ids" => [other_user.id] + }, + %{ + "name" => "\u26A0\uFE0F", + "count" => 1, + "me" => true, + "url" => nil, + "account_ids" => [other_user.id] + } + ] + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + ObanHelpers.perform_all() + + # Reacting with a custom emoji result = conn |> assign(:user, other_user) |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) - |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕") + |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/:dinosaur:") |> json_response_and_validate_schema(200) - # We return the status, but this our implementation detail. assert %{"id" => id} = result assert to_string(activity.id) == id assert result["pleroma"]["emoji_reactions"] == [ - %{"name" => "☕", "count" => 1, "me" => true} + %{ + "name" => "dinosaur", + "count" => 1, + "me" => true, + "url" => "http://localhost:4001/emoji/dino walking.gif", + "account_ids" => [other_user.id] + } ] + # Reacting with a remote emoji + note = + insert(:note, + user: user, + data: %{ + "reactions" => [ + ["👍", [other_user.ap_id], nil], + ["wow", [other_user.ap_id], "https://remote/emoji/wow"] + ] + } + ) + + activity = insert(:note_activity, note: note, user: user) + + result = + conn + |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:statuses"])) + |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/:wow@remote:") + |> json_response(200) + + assert result["pleroma"]["emoji_reactions"] == [ + %{ + "account_ids" => [other_user.id], + "count" => 1, + "me" => false, + "name" => "👍", + "url" => nil + }, + %{ + "name" => "wow@remote", + "count" => 2, + "me" => true, + "url" => "https://remote/emoji/wow", + "account_ids" => [user.id, other_user.id] + } + ] + + # Reacting with a remote custom emoji that hasn't been reacted with yet + note = + insert(:note, + user: user + ) + + activity = insert(:note_activity, note: note, user: user) + + assert conn + |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:statuses"])) + |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/:wow@remote:") + |> json_response(400) + # Reacting with a non-emoji assert conn |> assign(:user, other_user) @@ -46,8 +141,21 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do user = insert(:user) other_user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + note = + insert(:note, + user: user, + data: %{"reactions" => [["wow", [user.ap_id], "https://remote/emoji/wow"]]} + ) + + activity = insert(:note_activity, note: note, user: user) + + ObanHelpers.perform_all() + {:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + {:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, ":dinosaur:") + + {:ok, _reaction_activity} = + CommonAPI.react_with_emoji(activity.id, other_user, ":wow@remote:") ObanHelpers.perform_all() @@ -60,11 +168,47 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do assert %{"id" => id} = json_response_and_validate_schema(result, 200) assert to_string(activity.id) == id + # Remove custom emoji + + result = + conn + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) + |> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/:dinosaur:") + + assert %{"id" => id} = json_response_and_validate_schema(result, 200) + assert to_string(activity.id) == id + ObanHelpers.perform_all() object = Object.get_by_ap_id(activity.data["object"]) - assert object.data["reaction_count"] == 0 + assert object.data["reaction_count"] == 2 + + # Remove custom remote emoji + result = + conn + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) + |> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/:wow@remote:") + |> json_response(200) + + assert result["pleroma"]["emoji_reactions"] == [ + %{ + "name" => "wow@remote", + "count" => 1, + "me" => false, + "url" => "https://remote/emoji/wow", + "account_ids" => [user.id] + } + ] + + # Remove custom remote emoji that hasn't been reacted with yet + assert conn + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) + |> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/:zoop@remote:") + |> json_response(400) end test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do @@ -106,6 +250,38 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do result end + test "GET /api/v1/pleroma/statuses/:id/reactions with legacy format", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + note = + insert(:note, + user: user, + data: %{ + "reactions" => [["😿", [other_user.ap_id]]] + } + ) + + activity = insert(:note_activity, user: user, note: note) + + result = + conn + |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions") + |> json_response_and_validate_schema(200) + + other_user_id = other_user.id + + assert [ + %{ + "name" => "😿", + "count" => 1, + "me" => false, + "url" => nil, + "accounts" => [%{"id" => ^other_user_id}] + } + ] = result + end + test "GET /api/v1/pleroma/statuses/:id/reactions?with_muted=true", %{conn: conn} do user = insert(:user) user2 = insert(:user) @@ -181,7 +357,15 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") - assert [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = + assert [ + %{ + "name" => "🎅", + "count" => 1, + "accounts" => [represented_user], + "me" => false, + "url" => nil + } + ] = conn |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅") |> json_response_and_validate_schema(200) diff --git a/test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs index 365d26ab1..0d4951a73 100644 --- a/test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs @@ -16,7 +16,7 @@ defmodule Pleroma.Web.PleromaApi.InstancesControllerTest do {:ok, %Pleroma.Instances.Instance{unreachable_since: constant_unreachable}} = Instances.set_consistently_unreachable(constant) - _eventual_unrechable = Instances.set_unreachable(eventual) + _eventual_unreachable = Instances.set_unreachable(eventual) %{constant_unreachable: constant_unreachable, constant: constant} end @@ -26,6 +26,8 @@ defmodule Pleroma.Web.PleromaApi.InstancesControllerTest do constant_unreachable: constant_unreachable, constant: constant } do + clear_config([:instance, :public], false) + constant_host = URI.parse(constant).host assert conn diff --git a/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs index b72569d4b..81f09cdd1 100644 --- a/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs @@ -5,8 +5,11 @@ defmodule Pleroma.Web.PleromaAPI.MascotControllerTest do use Pleroma.Web.ConnCase, async: true + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.User + import Mox + test "mascot upload" do %{conn: conn} = oauth_access(["write:accounts"]) @@ -29,6 +32,9 @@ defmodule Pleroma.Web.PleromaAPI.MascotControllerTest do filename: "an_image.jpg" } + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + conn = conn |> put_req_header("content-type", "multipart/form-data") @@ -53,6 +59,9 @@ defmodule Pleroma.Web.PleromaAPI.MascotControllerTest do filename: "an_image.jpg" } + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + ret_conn = conn |> put_req_header("content-type", "multipart/form-data") diff --git a/test/pleroma/web/pleroma_api/controllers/notification_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/notification_controller_test.exs index b8c7964f9..036cbf176 100644 --- a/test/pleroma/web/pleroma_api/controllers/notification_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/notification_controller_test.exs @@ -21,13 +21,11 @@ defmodule Pleroma.Web.PleromaAPI.NotificationControllerTest do {:ok, [notification1]} = Notification.create_notifications(activity1) {:ok, [notification2]} = Notification.create_notifications(activity2) - response = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/notifications/read", %{id: notification1.id}) - |> json_response_and_validate_schema(:ok) + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/notifications/read", %{id: notification1.id}) + |> json_response_and_validate_schema(:ok) - assert %{"pleroma" => %{"is_seen" => true}} = response assert Repo.get(Notification, notification1.id).seen refute Repo.get(Notification, notification2.id).seen end @@ -40,14 +38,17 @@ defmodule Pleroma.Web.PleromaAPI.NotificationControllerTest do [notification3, notification2, notification1] = Notification.for_user(user1, %{limit: 3}) - [response1, response2] = - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/pleroma/notifications/read", %{max_id: notification2.id}) - |> json_response_and_validate_schema(:ok) + refute Repo.get(Notification, notification1.id).seen + refute Repo.get(Notification, notification2.id).seen + refute Repo.get(Notification, notification3.id).seen + + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/notifications/read", %{max_id: notification2.id}) + |> json_response_and_validate_schema(:ok) + + [notification3, notification2, notification1] = Notification.for_user(user1, %{limit: 3}) - assert %{"pleroma" => %{"is_seen" => true}} = response1 - assert %{"pleroma" => %{"is_seen" => true}} = response2 assert Repo.get(Notification, notification1.id).seen assert Repo.get(Notification, notification2.id).seen refute Repo.get(Notification, notification3.id).seen diff --git a/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs index 908ce962d..be94a02ad 100644 --- a/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs @@ -18,7 +18,8 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do "title" => "lain radio episode 1", "artist" => "lain", "album" => "lain radio", - "length" => "180000" + "length" => "180000", + "externalLink" => "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1" }) assert %{"title" => "lain radio episode 1"} = json_response_and_validate_schema(conn, 200) @@ -33,21 +34,24 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do CommonAPI.listen(user, %{ title: "lain radio episode 1", artist: "lain", - album: "lain radio" + album: "lain radio", + externalLink: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+1" }) {:ok, _activity} = CommonAPI.listen(user, %{ title: "lain radio episode 2", artist: "lain", - album: "lain radio" + album: "lain radio", + externalLink: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+2" }) {:ok, _activity} = CommonAPI.listen(user, %{ title: "lain radio episode 3", artist: "lain", - album: "lain radio" + album: "lain radio", + externalLink: "https://www.last.fm/music/lain/lain+radio/lain+radio+episode+3" }) conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/scrobbles") diff --git a/test/pleroma/web/pleroma_api/controllers/settings_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/settings_controller_test.exs new file mode 100644 index 000000000..e3c752d53 --- /dev/null +++ b/test/pleroma/web/pleroma_api/controllers/settings_controller_test.exs @@ -0,0 +1,126 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.SettingsControllerTest do + use Pleroma.Web.ConnCase + + import Pleroma.Factory + + describe "GET /api/v1/pleroma/settings/:app" do + setup do + oauth_access(["read:accounts"]) + end + + test "it gets empty settings", %{conn: conn} do + response = + conn + |> get("/api/v1/pleroma/settings/pleroma-fe") + |> json_response_and_validate_schema(:ok) + + assert response == %{} + end + + test "it gets settings", %{conn: conn, user: user} do + response = + conn + |> assign( + :user, + struct(user, + pleroma_settings_store: %{ + "pleroma-fe" => %{ + "foo" => "bar" + } + } + ) + ) + |> get("/api/v1/pleroma/settings/pleroma-fe") + |> json_response_and_validate_schema(:ok) + + assert %{"foo" => "bar"} == response + end + end + + describe "POST /api/v1/pleroma/settings/:app" do + setup do + settings = %{ + "foo" => "bar", + "nested" => %{ + "1" => "2" + } + } + + user = + insert( + :user, + %{ + pleroma_settings_store: %{ + "pleroma-fe" => settings + } + } + ) + + %{conn: conn} = oauth_access(["write:accounts"], user: user) + + %{conn: conn, user: user, settings: settings} + end + + test "it adds keys", %{conn: conn} do + response = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/v1/pleroma/settings/pleroma-fe", %{ + "foo" => "edited", + "bar" => "new", + "nested" => %{"3" => "4"} + }) + |> json_response_and_validate_schema(:ok) + + assert response == %{ + "foo" => "edited", + "bar" => "new", + "nested" => %{ + "1" => "2", + "3" => "4" + } + } + end + + test "it removes keys", %{conn: conn} do + response = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/v1/pleroma/settings/pleroma-fe", %{ + "foo" => nil, + "bar" => nil, + "nested" => %{ + "1" => nil, + "3" => nil + } + }) + |> json_response_and_validate_schema(:ok) + + assert response == %{ + "nested" => %{} + } + end + + test "it does not override settings for other apps", %{ + conn: conn, + user: user, + settings: settings + } do + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/v1/pleroma/settings/admin-fe", %{"foo" => "bar"}) + |> json_response_and_validate_schema(:ok) + + user = Pleroma.User.get_by_id(user.id) + + assert user.pleroma_settings_store == %{ + "pleroma-fe" => settings, + "admin-fe" => %{"foo" => "bar"} + } + end + end +end diff --git a/test/pleroma/web/pleroma_api/controllers/status_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/status_controller_test.exs new file mode 100644 index 000000000..f942f0556 --- /dev/null +++ b/test/pleroma/web/pleroma_api/controllers/status_controller_test.exs @@ -0,0 +1,54 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.StatusControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + describe "getting quotes of a specified post" do + setup do + [current_user, user] = insert_pair(:user) + %{user: current_user, conn: conn} = oauth_access(["read:statuses"], user: current_user) + [current_user: current_user, user: user, conn: conn] + end + + test "shows quotes of a post", %{conn: conn} do + user = insert(:user) + activity = insert(:note_activity) + + {:ok, quote_post} = CommonAPI.post(user, %{status: "quoat", quote_id: activity.id}) + + response = + conn + |> get("/api/v1/pleroma/statuses/#{activity.id}/quotes") + |> json_response_and_validate_schema(:ok) + + [status] = response + + assert length(response) == 1 + assert status["id"] == quote_post.id + end + + test "returns 404 error when a post can't be seen", %{conn: conn} do + activity = insert(:direct_note_activity) + + response = + conn + |> get("/api/v1/pleroma/statuses/#{activity.id}/quotes") + + assert json_response_and_validate_schema(response, 404) == %{"error" => "Record not found"} + end + + test "returns 404 error when a post does not exist", %{conn: conn} do + response = + conn + |> get("/api/v1/pleroma/statuses/idontexist/quotes") + + assert json_response_and_validate_schema(response, 404) == %{"error" => "Record not found"} + end + end +end diff --git a/test/pleroma/web/pleroma_api/views/backup_view_test.exs b/test/pleroma/web/pleroma_api/views/backup_view_test.exs index a86688bc4..b125b8872 100644 --- a/test/pleroma/web/pleroma_api/views/backup_view_test.exs +++ b/test/pleroma/web/pleroma_api/views/backup_view_test.exs @@ -4,10 +4,21 @@ defmodule Pleroma.Web.PleromaAPI.BackupViewTest do use Pleroma.DataCase, async: true + + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.User.Backup alias Pleroma.Web.PleromaAPI.BackupView + + import Mox import Pleroma.Factory + setup do + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + + :ok + end + test "it renders the ID" do user = insert(:user) backup = Backup.new(user) @@ -15,4 +26,43 @@ defmodule Pleroma.Web.PleromaAPI.BackupViewTest do result = BackupView.render("show.json", backup: backup) assert result.id == backup.id end + + test "it renders the state and processed_number" do + user = insert(:user) + backup = Backup.new(user) + + result = BackupView.render("show.json", backup: backup) + assert result.state == to_string(backup.state) + assert result.processed_number == backup.processed_number + end + + test "it renders failed state with legacy records" do + backup = %Backup{ + id: 0, + content_type: "application/zip", + file_name: "dummy", + file_size: 1, + state: :invalid, + processed: true, + processed_number: 1, + inserted_at: NaiveDateTime.utc_now() + } + + result = BackupView.render("show.json", backup: backup) + assert result.state == "complete" + + backup = %Backup{ + id: 0, + content_type: "application/zip", + file_name: "dummy", + file_size: 1, + state: :invalid, + processed: false, + processed_number: 1, + inserted_at: NaiveDateTime.utc_now() + } + + result = BackupView.render("show.json", backup: backup) + assert result.state == "failed" + end end diff --git a/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs b/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs index 017c9c5c0..f17add774 100644 --- a/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs +++ b/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs @@ -3,28 +3,36 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceViewTest do - use Pleroma.DataCase + alias Pleroma.NullCache + use Pleroma.DataCase, async: true alias Pleroma.Chat alias Pleroma.Chat.MessageReference alias Pleroma.Object + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView + import Mox import Pleroma.Factory + setup do: clear_config([:rich_media, :enabled], true) + test "it displays a chat message" do user = insert(:user) recipient = insert(:user) + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + file = %Plug.Upload{ content_type: "image/jpeg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } - {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) + {:ok, upload} = ActivityPub.upload(file, actor: recipient.ap_id) {:ok, activity} = CommonAPI.post_chat_message(user, recipient, "kippis :firefox:", idempotency_key: "123") @@ -35,6 +43,15 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceViewTest do cm_ref = MessageReference.for_chat_and_object(chat, object) + id = cm_ref.id + + Pleroma.CachexMock + |> stub(:get, fn + :chat_message_id_idempotency_key_cache, ^id -> {:ok, "123"} + cache, key -> NullCache.get(cache, key) + end) + |> stub(:fetch, fn :rich_media_cache, _, _ -> {:ok, {:ok, %{}}} end) + chat_message = MessageReferenceView.render("show.json", chat_message_reference: cm_ref) assert chat_message[:id] == cm_ref.id @@ -46,12 +63,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceViewTest do assert match?([%{shortcode: "firefox"}], chat_message[:emojis]) assert chat_message[:idempotency_key] == "123" - clear_config([:rich_media, :enabled], true) - - Tesla.Mock.mock_global(fn - %{url: "https://example.com/ogp"} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")} - end) + Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) {:ok, activity} = CommonAPI.post_chat_message(recipient, user, "gkgkgk https://example.com/ogp", diff --git a/test/pleroma/web/plugs/authentication_plug_test.exs b/test/pleroma/web/plugs/authentication_plug_test.exs index 41fdb93bc..b8acd01c5 100644 --- a/test/pleroma/web/plugs/authentication_plug_test.exs +++ b/test/pleroma/web/plugs/authentication_plug_test.exs @@ -70,28 +70,6 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlugTest do assert "$pbkdf2" <> _ = user.password_hash end - @tag :skip_on_mac - test "with a crypt hash, it updates to a pkbdf2 hash", %{conn: conn} do - user = - insert(:user, - password_hash: - "$6$9psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1" - ) - - conn = - conn - |> assign(:auth_user, user) - |> assign(:auth_credentials, %{password: "password"}) - |> AuthenticationPlug.call(%{}) - - assert conn.assigns.user.id == conn.assigns.auth_user.id - assert conn.assigns.token == nil - assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug) - - user = User.get_by_id(user.id) - assert "$pbkdf2" <> _ = user.password_hash - end - describe "checkpw/2" do test "check pbkdf2 hash" do hash = @@ -101,14 +79,6 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlugTest do refute AuthenticationPlug.checkpw("test-password1", hash) end - @tag :skip_on_mac - test "check sha512-crypt hash" do - hash = - "$6$9psBWV8gxkGOZWBz$PmfCycChoxeJ3GgGzwvhlgacb9mUoZ.KUXNCssekER4SJ7bOK53uXrHNb2e4i8yPFgSKyzaW9CcmrDXWIEMtD1" - - assert AuthenticationPlug.checkpw("password", hash) - end - test "check bcrypt hash" do hash = "$2a$10$uyhC/R/zoE1ndwwCtMusK.TLVzkQ/Ugsbqp3uXI.CTTz0gBw.24jS" diff --git a/test/pleroma/web/plugs/ensure_privileged_plug_test.exs b/test/pleroma/web/plugs/ensure_privileged_plug_test.exs new file mode 100644 index 000000000..bba972fad --- /dev/null +++ b/test/pleroma/web/plugs/ensure_privileged_plug_test.exs @@ -0,0 +1,96 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.EnsurePrivilegedPlugTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.Plugs.EnsurePrivilegedPlug + import Pleroma.Factory + + test "denies a user that isn't moderator or admin" do + clear_config([:instance, :admin_privileges], []) + user = insert(:user) + + conn = + build_conn() + |> assign(:user, user) + |> EnsurePrivilegedPlug.call(:cofe) + + assert conn.status == 403 + end + + test "accepts an admin that is privileged" do + clear_config([:instance, :admin_privileges], [:cofe]) + user = insert(:user, is_admin: true) + conn = assign(build_conn(), :user, user) + + ret_conn = EnsurePrivilegedPlug.call(conn, :cofe) + + assert conn == ret_conn + end + + test "denies an admin that isn't privileged" do + clear_config([:instance, :admin_privileges], [:suya]) + user = insert(:user, is_admin: true) + + conn = + build_conn() + |> assign(:user, user) + |> EnsurePrivilegedPlug.call(:cofe) + + assert conn.status == 403 + end + + test "accepts a moderator that is privileged" do + clear_config([:instance, :moderator_privileges], [:cofe]) + user = insert(:user, is_moderator: true) + conn = assign(build_conn(), :user, user) + + ret_conn = EnsurePrivilegedPlug.call(conn, :cofe) + + assert conn == ret_conn + end + + test "denies a moderator that isn't privileged" do + clear_config([:instance, :moderator_privileges], [:suya]) + user = insert(:user, is_moderator: true) + + conn = + build_conn() + |> assign(:user, user) + |> EnsurePrivilegedPlug.call(:cofe) + + assert conn.status == 403 + end + + test "accepts for a privileged role even if other role isn't privileged" do + clear_config([:instance, :admin_privileges], [:cofe]) + clear_config([:instance, :moderator_privileges], []) + user = insert(:user, is_admin: true, is_moderator: true) + conn = assign(build_conn(), :user, user) + + ret_conn = EnsurePrivilegedPlug.call(conn, :cofe) + + # privileged through admin role + assert conn == ret_conn + + clear_config([:instance, :admin_privileges], []) + clear_config([:instance, :moderator_privileges], [:cofe]) + user = insert(:user, is_admin: true, is_moderator: true) + conn = assign(build_conn(), :user, user) + + ret_conn = EnsurePrivilegedPlug.call(conn, :cofe) + + # privileged through moderator role + assert conn == ret_conn + end + + test "denies when no user is set" do + conn = + build_conn() + |> EnsurePrivilegedPlug.call(:cofe) + + assert conn.status == 403 + end +end diff --git a/test/pleroma/web/plugs/ensure_staff_privileged_plug_test.exs b/test/pleroma/web/plugs/ensure_staff_privileged_plug_test.exs deleted file mode 100644 index c684714b8..000000000 --- a/test/pleroma/web/plugs/ensure_staff_privileged_plug_test.exs +++ /dev/null @@ -1,60 +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.Web.Plugs.EnsureStaffPrivilegedPlugTest do - use Pleroma.Web.ConnCase, async: true - - alias Pleroma.Web.Plugs.EnsureStaffPrivilegedPlug - import Pleroma.Factory - - test "accepts a user that is an admin" do - user = insert(:user, is_admin: true) - - conn = assign(build_conn(), :user, user) - - ret_conn = EnsureStaffPrivilegedPlug.call(conn, %{}) - - assert conn == ret_conn - end - - test "accepts a user that is a moderator when :privileged_staff is enabled" do - clear_config([:instance, :privileged_staff], true) - user = insert(:user, is_moderator: true) - - conn = assign(build_conn(), :user, user) - - ret_conn = EnsureStaffPrivilegedPlug.call(conn, %{}) - - assert conn == ret_conn - end - - test "denies a user that is a moderator when :privileged_staff is disabled" do - clear_config([:instance, :privileged_staff], false) - user = insert(:user, is_moderator: true) - - conn = - build_conn() - |> assign(:user, user) - |> EnsureStaffPrivilegedPlug.call(%{}) - - assert conn.status == 403 - end - - test "denies a user that isn't a staff member" do - user = insert(:user) - - conn = - build_conn() - |> assign(:user, user) - |> EnsureStaffPrivilegedPlug.call(%{}) - - assert conn.status == 403 - end - - test "denies when a user isn't set" do - conn = EnsureStaffPrivilegedPlug.call(build_conn(), %{}) - - assert conn.status == 403 - end -end diff --git a/test/pleroma/web/plugs/frontend_static_plug_test.exs b/test/pleroma/web/plugs/frontend_static_plug_test.exs index a91e24c49..6f4d24d9e 100644 --- a/test/pleroma/web/plugs/frontend_static_plug_test.exs +++ b/test/pleroma/web/plugs/frontend_static_plug_test.exs @@ -4,7 +4,11 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do use Pleroma.Web.ConnCase + import Mock + import Mox + + alias Pleroma.UnstubbedConfigMock, as: ConfigMock @dir "test/tmp/instance_static" @@ -66,6 +70,9 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do File.mkdir_p!("#{path}/proxy/rr/ss") File.write!("#{path}/proxy/rr/ss/Ek7w8WPVcAApOvN.jpg:large", "FB image") + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + url = Pleroma.Web.MediaProxy.encode_url("https://pbs.twimg.com/media/Ek7w8WPVcAApOvN.jpg:large") @@ -82,12 +89,11 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do "api", "main", "ostatus_subscribe", + "authorize_interaction", "oauth", "objects", "activities", "notice", - "@:nickname", - ":nickname", "users", "tags", "mailer", diff --git a/test/pleroma/web/plugs/http_security_plug_test.exs b/test/pleroma/web/plugs/http_security_plug_test.exs index e1e97c1ce..c79170382 100644 --- a/test/pleroma/web/plugs/http_security_plug_test.exs +++ b/test/pleroma/web/plugs/http_security_plug_test.exs @@ -59,9 +59,9 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do assert csp =~ ~r|report-uri https://endpoint.com;report-to csp-endpoint;| - [reply_to] = Conn.get_resp_header(conn, "reply-to") + [report_to] = Conn.get_resp_header(conn, "report-to") - assert reply_to == + assert report_to == "{\"endpoints\":[{\"url\":\"https://endpoint.com\"}],\"group\":\"csp-endpoint\",\"max-age\":10886400}" end diff --git a/test/pleroma/web/plugs/rate_limiter_test.exs b/test/pleroma/web/plugs/rate_limiter_test.exs index b1ac76120..19cee8aee 100644 --- a/test/pleroma/web/plugs/rate_limiter_test.exs +++ b/test/pleroma/web/plugs/rate_limiter_test.exs @@ -48,38 +48,42 @@ defmodule Pleroma.Web.Plugs.RateLimiterTest do refute RateLimiter.disabled?(build_conn()) end - @tag :erratic test "it restricts based on config values" do limiter_name = :test_plug_opts scale = 80 limit = 5 - clear_config([Pleroma.Web.Endpoint, :http, :ip], {8, 8, 8, 8}) + clear_config([Pleroma.Web.Endpoint, :http, :ip], {127, 0, 0, 1}) clear_config([:rate_limit, limiter_name], {scale, limit}) plug_opts = RateLimiter.init(name: limiter_name) conn = build_conn(:get, "/") - for i <- 1..5 do - conn = RateLimiter.call(conn, plug_opts) - assert {^i, _} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) - Process.sleep(10) + for _ <- 1..5 do + conn_limited = RateLimiter.call(conn, plug_opts) + + refute conn_limited.status == Conn.Status.code(:too_many_requests) + refute conn_limited.resp_body + refute conn_limited.halted end - conn = RateLimiter.call(conn, plug_opts) - assert %{"error" => "Throttled"} = ConnTest.json_response(conn, :too_many_requests) - assert conn.halted + conn_limited = RateLimiter.call(conn, plug_opts) + assert %{"error" => "Throttled"} = ConnTest.json_response(conn_limited, :too_many_requests) + assert conn_limited.halted - Process.sleep(50) + expire_ttl(conn, limiter_name) - conn = build_conn(:get, "/") + for _ <- 1..5 do + conn_limited = RateLimiter.call(conn, plug_opts) - conn = RateLimiter.call(conn, plug_opts) - assert {1, 4} = RateLimiter.inspect_bucket(conn, limiter_name, plug_opts) + refute conn_limited.status == Conn.Status.code(:too_many_requests) + refute conn_limited.resp_body + refute conn_limited.halted + end - refute conn.status == Conn.Status.code(:too_many_requests) - refute conn.resp_body - refute conn.halted + conn_limited = RateLimiter.call(conn, plug_opts) + assert %{"error" => "Throttled"} = ConnTest.json_response(conn_limited, :too_many_requests) + assert conn_limited.halted end describe "options" do @@ -263,4 +267,12 @@ defmodule Pleroma.Web.Plugs.RateLimiterTest do refute {:err, :not_found} == RateLimiter.inspect_bucket(conn, limiter_name, opts) end + + def expire_ttl(%{remote_ip: remote_ip} = _conn, bucket_name_root) do + bucket_name = "anon:#{bucket_name_root}" |> String.to_atom() + key_name = "ip::#{remote_ip |> Tuple.to_list() |> Enum.join(".")}" + + {:ok, bucket_value} = Cachex.get(bucket_name, key_name) + Cachex.put(bucket_name, key_name, bucket_value, ttl: -1) + end end diff --git a/test/pleroma/web/plugs/uploaded_media_plug_test.exs b/test/pleroma/web/plugs/uploaded_media_plug_test.exs index ec46b0537..6a9366e28 100644 --- a/test/pleroma/web/plugs/uploaded_media_plug_test.exs +++ b/test/pleroma/web/plugs/uploaded_media_plug_test.exs @@ -4,10 +4,18 @@ defmodule Pleroma.Web.Plugs.UploadedMediaPlugTest do use Pleroma.Web.ConnCase, async: true + + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.Upload + import Mox + defp upload_file(context) do + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + Pleroma.DataCase.ensure_local_uploader(context) + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") file = %Plug.Upload{ @@ -23,6 +31,13 @@ defmodule Pleroma.Web.Plugs.UploadedMediaPlugTest do setup_all :upload_file + setup do + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + + :ok + end + test "does not send Content-Disposition header when name param is not set", %{ attachment_url: attachment_url } do @@ -33,11 +48,11 @@ defmodule Pleroma.Web.Plugs.UploadedMediaPlugTest do test "sends Content-Disposition header when name param is set", %{ attachment_url: attachment_url } do - conn = get(build_conn(), attachment_url <> "?name=\"cofe\".gif") + conn = get(build_conn(), attachment_url <> ~s[?name="cofe".gif]) assert Enum.any?( conn.resp_headers, - &(&1 == {"content-disposition", "filename=\"\\\"cofe\\\".gif\""}) + &(&1 == {"content-disposition", ~s[inline; filename="\\"cofe\\".gif"]}) ) end end diff --git a/test/pleroma/web/push/impl_test.exs b/test/pleroma/web/push/impl_test.exs index b8112cce5..3ceea3d71 100644 --- a/test/pleroma/web/push/impl_test.exs +++ b/test/pleroma/web/push/impl_test.exs @@ -5,10 +5,12 @@ defmodule Pleroma.Web.Push.ImplTest do use Pleroma.DataCase, async: true + import Mox import Pleroma.Factory alias Pleroma.Notification alias Pleroma.Object + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI @@ -202,6 +204,21 @@ defmodule Pleroma.Web.Push.ImplTest do "New Reaction" end + test "renders title and body for update activity" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{status: "lorem ipsum"}) + + {:ok, activity} = CommonAPI.update(user, activity, %{status: "edited status"}) + object = Object.normalize(activity, fetch: false) + + assert Impl.format_body(%{activity: activity, type: "update"}, user, object) == + "@#{user.nickname} edited a status" + + assert Impl.format_title(%{activity: activity, type: "update"}) == + "New Update" + end + test "renders title for create activity with direct visibility" do user = insert(:user, nickname: "Bob") @@ -242,6 +259,9 @@ defmodule Pleroma.Web.Push.ImplTest do filename: "an_image.jpg" } + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) {:ok, chat} = CommonAPI.post_chat_message(user, recipient, nil, media_id: upload.id) diff --git a/test/pleroma/web/rich_media/card_test.exs b/test/pleroma/web/rich_media/card_test.exs new file mode 100644 index 000000000..516ac9951 --- /dev/null +++ b/test/pleroma/web/rich_media/card_test.exs @@ -0,0 +1,71 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.CardTest do + use Pleroma.DataCase, async: true + + alias Pleroma.UnstubbedConfigMock, as: ConfigMock + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.RichMedia.Card + + import Mox + import Pleroma.Factory + import Tesla.Mock + + setup do + mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) + + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + + :ok + end + + setup do: clear_config([:rich_media, :enabled], true) + + test "crawls URL in activity" do + user = insert(:user) + + url = "https://example.com/ogp" + url_hash = Card.url_to_hash(url) + + {:ok, activity} = + CommonAPI.post(user, %{ + status: "[test](#{url})", + content_type: "text/markdown" + }) + + assert %Card{url_hash: ^url_hash, fields: _} = Card.get_by_activity(activity) + end + + test "recrawls URLs on status edits/updates" do + original_url = "https://google.com/" + original_url_hash = Card.url_to_hash(original_url) + updated_url = "https://yahoo.com/" + updated_url_hash = Card.url_to_hash(updated_url) + + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{status: "I like this site #{original_url}"}) + + # Force a backfill + Card.get_by_activity(activity) + + assert match?( + %Card{url_hash: ^original_url_hash, fields: _}, + Card.get_by_activity(activity) + ) + + {:ok, _} = CommonAPI.update(user, activity, %{status: "I like this site #{updated_url}"}) + + activity = Pleroma.Activity.get_by_id(activity.id) + + # Force a backfill + Card.get_by_activity(activity) + + assert match?( + %Card{url_hash: ^updated_url_hash, fields: _}, + Card.get_by_activity(activity) + ) + end +end diff --git a/test/pleroma/web/rich_media/helpers_test.exs b/test/pleroma/web/rich_media/helpers_test.exs deleted file mode 100644 index 630b3ca95..000000000 --- a/test/pleroma/web/rich_media/helpers_test.exs +++ /dev/null @@ -1,84 +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.Web.RichMedia.HelpersTest do - use Pleroma.DataCase - - alias Pleroma.Web.CommonAPI - alias Pleroma.Web.RichMedia.Helpers - - import Pleroma.Factory - import Tesla.Mock - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - - :ok - end - - setup do: clear_config([:rich_media, :enabled]) - - test "refuses to crawl incomplete URLs" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "[test](example.com/ogp)", - content_type: "text/markdown" - }) - - clear_config([:rich_media, :enabled], true) - - assert %{} == Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) - end - - test "refuses to crawl malformed URLs" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "[test](example.com[]/ogp)", - content_type: "text/markdown" - }) - - clear_config([:rich_media, :enabled], true) - - assert %{} == Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) - end - - test "crawls valid, complete URLs" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{ - status: "[test](https://example.com/ogp)", - content_type: "text/markdown" - }) - - clear_config([:rich_media, :enabled], true) - - assert %{page_url: "https://example.com/ogp", rich_media: _} = - Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) - end - - test "refuses to crawl URLs of private network from posts" do - user = insert(:user) - - {:ok, activity} = - CommonAPI.post(user, %{status: "http://127.0.0.1:4000/notice/9kCP7VNyPJXFOXDrgO"}) - - {:ok, activity2} = CommonAPI.post(user, %{status: "https://10.111.10.1/notice/9kCP7V"}) - {:ok, activity3} = CommonAPI.post(user, %{status: "https://172.16.32.40/notice/9kCP7V"}) - {:ok, activity4} = CommonAPI.post(user, %{status: "https://192.168.10.40/notice/9kCP7V"}) - {:ok, activity5} = CommonAPI.post(user, %{status: "https://pleroma.local/notice/9kCP7V"}) - - clear_config([:rich_media, :enabled], true) - - assert %{} = Helpers.fetch_data_for_activity(activity) - assert %{} = Helpers.fetch_data_for_activity(activity2) - assert %{} = Helpers.fetch_data_for_activity(activity3) - assert %{} = Helpers.fetch_data_for_activity(activity4) - assert %{} = Helpers.fetch_data_for_activity(activity5) - end -end diff --git a/test/pleroma/web/rich_media/parser/ttl/aws_signed_url_test.exs b/test/pleroma/web/rich_media/parser/ttl/aws_signed_url_test.exs index 59b3330ba..cc28aa7f3 100644 --- a/test/pleroma/web/rich_media/parser/ttl/aws_signed_url_test.exs +++ b/test/pleroma/web/rich_media/parser/ttl/aws_signed_url_test.exs @@ -3,8 +3,23 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrlTest do - # Relies on Cachex, needs to be synchronous - use Pleroma.DataCase + use Pleroma.DataCase, async: false + use Oban.Testing, repo: Pleroma.Repo + + import Mox + + alias Pleroma.UnstubbedConfigMock, as: ConfigMock + alias Pleroma.Web.RichMedia.Card + alias Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl + + setup do + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + + clear_config([:rich_media, :enabled], true) + + :ok + end test "s3 signed url is parsed correct for expiration time" do url = "https://pleroma.social/amz" @@ -22,7 +37,7 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrlTest do expire_time = Timex.parse!(timestamp, "{ISO:Basic:Z}") |> Timex.to_unix() |> Kernel.+(valid_till) - assert {:ok, expire_time} == Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl.ttl(metadata, url) + assert expire_time == Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl.ttl(metadata, url) end test "s3 signed url is parsed and correct ttl is set for rich media" do @@ -43,26 +58,35 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrlTest do <meta name="twitter:site" content="Pleroma" /> <meta name="twitter:title" content="Pleroma" /> <meta name="twitter:description" content="Pleroma" /> - <meta name="twitter:image" content="#{Map.get(metadata, :image)}" /> + <meta name="twitter:image" content="#{Map.get(metadata, "image")}" /> """ Tesla.Mock.mock(fn %{ method: :get, - url: "https://pleroma.social/amz" + url: ^url } -> %Tesla.Env{status: 200, body: body} + + %{method: :head} -> + %Tesla.Env{status: 200} end) - Cachex.put(:rich_media_cache, url, metadata) + Card.get_or_backfill_by_url(url) + + assert_enqueued(worker: Pleroma.Workers.RichMediaExpirationWorker, args: %{"url" => url}) + + [%Oban.Job{scheduled_at: scheduled_at}] = all_enqueued() - Pleroma.Web.RichMedia.Parser.set_ttl_based_on_image(metadata, url) + timestamp_dt = Timex.parse!(timestamp, "{ISO:Basic:Z}") + + assert DateTime.diff(scheduled_at, timestamp_dt) == valid_till + end - {:ok, cache_ttl} = Cachex.ttl(:rich_media_cache, url) + test "AWS URL for an image without expiration works" do + og_data = %{"image" => "https://amazonaws.com/image.png"} - # as there is delay in setting and pulling the data from cache we ignore 1 second - # make it 2 seconds for flakyness - assert_in_delta(valid_till * 1000, cache_ttl, 2000) + assert is_nil(AwsSignedUrl.ttl(og_data, "")) end defp construct_s3_url(timestamp, valid_till) do @@ -71,11 +95,11 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrlTest do defp construct_metadata(timestamp, valid_till, url) do %{ - image: construct_s3_url(timestamp, valid_till), - site: "Pleroma", - title: "Pleroma", - description: "Pleroma", - url: url + "image" => construct_s3_url(timestamp, valid_till), + "site" => "Pleroma", + "title" => "Pleroma", + "description" => "Pleroma", + "url" => url } end end diff --git a/test/pleroma/web/rich_media/parser/ttl/opengraph_test.exs b/test/pleroma/web/rich_media/parser/ttl/opengraph_test.exs new file mode 100644 index 000000000..770968d47 --- /dev/null +++ b/test/pleroma/web/rich_media/parser/ttl/opengraph_test.exs @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.Parser.TTL.OpengraphTest do + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + + import Mox + + alias Pleroma.UnstubbedConfigMock, as: ConfigMock + alias Pleroma.Web.RichMedia.Card + + setup do + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + + clear_config([:rich_media, :enabled], true) + + :ok + end + + test "OpenGraph TTL value is honored" do + url = "https://reddit.com/r/somepost" + + Tesla.Mock.mock(fn + %{ + method: :get, + url: ^url + } -> + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/reddit.html")} + + %{method: :head} -> + %Tesla.Env{status: 200} + end) + + Card.get_or_backfill_by_url(url) + + assert_enqueued(worker: Pleroma.Workers.RichMediaExpirationWorker, args: %{"url" => url}) + end +end diff --git a/test/pleroma/web/rich_media/parser_test.exs b/test/pleroma/web/rich_media/parser_test.exs index ffdc4e5d7..3fcb5c808 100644 --- a/test/pleroma/web/rich_media/parser_test.exs +++ b/test/pleroma/web/rich_media/parser_test.exs @@ -3,95 +3,26 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.RichMedia.ParserTest do - use ExUnit.Case, async: true + use Pleroma.DataCase alias Pleroma.Web.RichMedia.Parser - setup do - Tesla.Mock.mock(fn - %{ - method: :get, - url: "http://example.com/ogp" - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")} - - %{ - method: :get, - url: "http://example.com/non-ogp" - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/non_ogp_embed.html")} - - %{ - method: :get, - url: "http://example.com/ogp-missing-title" - } -> - %Tesla.Env{ - status: 200, - body: File.read!("test/fixtures/rich_media/ogp-missing-title.html") - } - - %{ - method: :get, - url: "http://example.com/twitter-card" - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")} - - %{ - method: :get, - url: "http://example.com/oembed" - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/oembed.html")} - - %{ - method: :get, - url: "http://example.com/oembed.json" - } -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/oembed.json")} - - %{method: :get, url: "http://example.com/empty"} -> - %Tesla.Env{status: 200, body: "hello"} - - %{method: :get, url: "http://example.com/malformed"} -> - %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/malformed-data.html")} - - %{method: :get, url: "http://example.com/error"} -> - {:error, :overload} - - %{ - method: :head, - url: "http://example.com/huge-page" - } -> - %Tesla.Env{ - status: 200, - headers: [{"content-length", "2000001"}, {"content-type", "text/html"}] - } - - %{ - method: :head, - url: "http://example.com/pdf-file" - } -> - %Tesla.Env{ - status: 200, - headers: [{"content-length", "1000000"}, {"content-type", "application/pdf"}] - } - - %{method: :head} -> - %Tesla.Env{status: 404, body: "", headers: []} - end) + import Tesla.Mock - :ok + setup do + mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) end test "returns error when no metadata present" do - assert {:error, _} = Parser.parse("http://example.com/empty") + assert {:error, _} = Parser.parse("https://example.com/empty") end test "doesn't just add a title" do - assert {:error, {:invalid_metadata, _}} = Parser.parse("http://example.com/non-ogp") + assert {:error, {:invalid_metadata, _}} = Parser.parse("https://example.com/non-ogp") end test "parses ogp" do - assert Parser.parse("http://example.com/ogp") == + assert Parser.parse("https://example.com/ogp") == {:ok, %{ "image" => "http://ia.media-imdb.com/images/rock.jpg", @@ -99,12 +30,12 @@ defmodule Pleroma.Web.RichMedia.ParserTest do "description" => "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.", "type" => "video.movie", - "url" => "http://example.com/ogp" + "url" => "https://example.com/ogp" }} end test "falls back to <title> when ogp:title is missing" do - assert Parser.parse("http://example.com/ogp-missing-title") == + assert Parser.parse("https://example.com/ogp-missing-title") == {:ok, %{ "image" => "http://ia.media-imdb.com/images/rock.jpg", @@ -112,12 +43,12 @@ defmodule Pleroma.Web.RichMedia.ParserTest do "description" => "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.", "type" => "video.movie", - "url" => "http://example.com/ogp-missing-title" + "url" => "https://example.com/ogp-missing-title" }} end test "parses twitter card" do - assert Parser.parse("http://example.com/twitter-card") == + assert Parser.parse("https://example.com/twitter-card") == {:ok, %{ "card" => "summary", @@ -125,12 +56,12 @@ defmodule Pleroma.Web.RichMedia.ParserTest do "image" => "https://farm6.staticflickr.com/5510/14338202952_93595258ff_z.jpg", "title" => "Small Island Developing States Photo Submission", "description" => "View the album on Flickr.", - "url" => "http://example.com/twitter-card" + "url" => "https://example.com/twitter-card" }} end - test "parses OEmbed" do - assert Parser.parse("http://example.com/oembed") == + test "parses OEmbed and filters HTML tags" do + assert Parser.parse("https://example.com/oembed") == {:ok, %{ "author_name" => "\u202E\u202D\u202Cbees\u202C", @@ -139,7 +70,7 @@ defmodule Pleroma.Web.RichMedia.ParserTest do "flickr_type" => "photo", "height" => "768", "html" => - "<a data-flickr-embed=\"true\" href=\"https://www.flickr.com/photos/bees/2362225867/\" title=\"Bacon Lollys by \u202E\u202D\u202Cbees\u202C, on Flickr\"><img src=\"https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg\" width=\"1024\" height=\"768\" alt=\"Bacon Lollys\"></a><script async src=\"https://embedr.flickr.com/assets/client-code.js\" charset=\"utf-8\"></script>", + "<a href=\"https://www.flickr.com/photos/bees/2362225867/\" title=\"Bacon Lollys by \u202E\u202D\u202Cbees\u202C, on Flickr\"><img src=\"https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg\" width=\"1024\" height=\"768\" alt=\"Bacon Lollys\"/></a>", "license" => "All Rights Reserved", "license_id" => 0, "provider_name" => "Flickr", @@ -150,7 +81,7 @@ defmodule Pleroma.Web.RichMedia.ParserTest do "thumbnail_width" => 150, "title" => "Bacon Lollys", "type" => "photo", - "url" => "http://example.com/oembed", + "url" => "https://example.com/oembed", "version" => "1.0", "web_page" => "https://www.flickr.com/photos/bees/2362225867/", "web_page_short_url" => "https://flic.kr/p/4AK2sc", @@ -159,18 +90,41 @@ defmodule Pleroma.Web.RichMedia.ParserTest do end test "rejects invalid OGP data" do - assert {:error, _} = Parser.parse("http://example.com/malformed") + assert {:error, _} = Parser.parse("https://example.com/malformed") end test "returns error if getting page was not successful" do - assert {:error, :overload} = Parser.parse("http://example.com/error") + assert {:error, :overload} = Parser.parse("https://example.com/error") end test "does a HEAD request to check if the body is too large" do - assert {:error, :body_too_large} = Parser.parse("http://example.com/huge-page") + assert {:error, :body_too_large} = Parser.parse("https://example.com/huge-page") end test "does a HEAD request to check if the body is html" do - assert {:error, {:content_type, _}} = Parser.parse("http://example.com/pdf-file") + assert {:error, {:content_type, _}} = Parser.parse("https://example.com/pdf-file") + end + + test "refuses to crawl incomplete URLs" do + url = "example.com/ogp" + assert :error == Parser.parse(url) + end + + test "refuses to crawl malformed URLs" do + url = "example.com[]/ogp" + assert :error == Parser.parse(url) + end + + test "refuses to crawl URLs of private network from posts" do + [ + "http://127.0.0.1:4000/notice/9kCP7VNyPJXFOXDrgO", + "https://10.111.10.1/notice/9kCP7V", + "https://172.16.32.40/notice/9kCP7V", + "https://192.168.10.40/notice/9kCP7V", + "https://pleroma.local/notice/9kCP7V" + ] + |> Enum.each(fn url -> + assert :error == Parser.parse(url) + end) end end diff --git a/test/pleroma/web/streamer_test.exs b/test/pleroma/web/streamer_test.exs index 4d4fed070..d85358fd4 100644 --- a/test/pleroma/web/streamer_test.exs +++ b/test/pleroma/web/streamer_test.exs @@ -22,6 +22,10 @@ defmodule Pleroma.Web.StreamerTest do setup do: clear_config([:instance, :skip_thread_containment]) describe "get_topic/_ (unauthenticated)" do + test "allows no stream" do + assert {:ok, nil} = Streamer.get_topic(nil, nil, nil) + end + test "allows public" do assert {:ok, "public"} = Streamer.get_topic("public", nil, nil) assert {:ok, "public:local"} = Streamer.get_topic("public:local", nil, nil) @@ -29,6 +33,26 @@ defmodule Pleroma.Web.StreamerTest do assert {:ok, "public:local:media"} = Streamer.get_topic("public:local:media", nil, nil) end + test "rejects local public streams if restricted_unauthenticated is on" do + clear_config([:restrict_unauthenticated, :timelines, :local], true) + + assert {:error, :unauthorized} = Streamer.get_topic("public:local", nil, nil) + assert {:error, :unauthorized} = Streamer.get_topic("public:local:media", nil, nil) + end + + test "rejects remote public streams if restricted_unauthenticated is on" do + clear_config([:restrict_unauthenticated, :timelines, :federated], true) + + assert {:error, :unauthorized} = Streamer.get_topic("public", nil, nil) + assert {:error, :unauthorized} = Streamer.get_topic("public:media", nil, nil) + + assert {:error, :unauthorized} = + Streamer.get_topic("public:remote", nil, nil, %{"instance" => "lain.com"}) + + assert {:error, :unauthorized} = + Streamer.get_topic("public:remote:media", nil, nil, %{"instance" => "lain.com"}) + end + test "allows instance streams" do assert {:ok, "public:remote:lain.com"} = Streamer.get_topic("public:remote", nil, nil, %{"instance" => "lain.com"}) @@ -69,6 +93,63 @@ defmodule Pleroma.Web.StreamerTest do end end + test "allows local public streams if restricted_unauthenticated is on", %{ + user: user, + token: oauth_token + } do + clear_config([:restrict_unauthenticated, :timelines, :local], true) + + %{token: read_notifications_token} = oauth_access(["read:notifications"], user: user) + %{token: badly_scoped_token} = oauth_access(["irrelevant:scope"], user: user) + + assert {:ok, "public:local"} = Streamer.get_topic("public:local", user, oauth_token) + + assert {:ok, "public:local:media"} = + Streamer.get_topic("public:local:media", user, oauth_token) + + for token <- [read_notifications_token, badly_scoped_token] do + assert {:error, :unauthorized} = Streamer.get_topic("public:local", user, token) + + assert {:error, :unauthorized} = Streamer.get_topic("public:local:media", user, token) + end + end + + test "allows remote public streams if restricted_unauthenticated is on", %{ + user: user, + token: oauth_token + } do + clear_config([:restrict_unauthenticated, :timelines, :federated], true) + + %{token: read_notifications_token} = oauth_access(["read:notifications"], user: user) + %{token: badly_scoped_token} = oauth_access(["irrelevant:scope"], user: user) + + assert {:ok, "public"} = Streamer.get_topic("public", user, oauth_token) + assert {:ok, "public:media"} = Streamer.get_topic("public:media", user, oauth_token) + + assert {:ok, "public:remote:lain.com"} = + Streamer.get_topic("public:remote", user, oauth_token, %{"instance" => "lain.com"}) + + assert {:ok, "public:remote:media:lain.com"} = + Streamer.get_topic("public:remote:media", user, oauth_token, %{ + "instance" => "lain.com" + }) + + for token <- [read_notifications_token, badly_scoped_token] do + assert {:error, :unauthorized} = Streamer.get_topic("public", user, token) + assert {:error, :unauthorized} = Streamer.get_topic("public:media", user, token) + + assert {:error, :unauthorized} = + Streamer.get_topic("public:remote", user, token, %{ + "instance" => "lain.com" + }) + + assert {:error, :unauthorized} = + Streamer.get_topic("public:remote:media", user, token, %{ + "instance" => "lain.com" + }) + end + end + test "allows user streams (with proper OAuth token scopes)", %{ user: user, token: read_oauth_token @@ -165,7 +246,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user", user, oauth_token) {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} refute Streamer.filtered_by_user?(user, activity) end @@ -176,7 +257,7 @@ defmodule Pleroma.Web.StreamerTest do {:ok, activity} = CommonAPI.post(other_user, %{status: "hey"}) {:ok, announce} = CommonAPI.repeat(activity.id, user) - assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce} + assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce, _} refute Streamer.filtered_by_user?(user, announce) end @@ -229,7 +310,7 @@ defmodule Pleroma.Web.StreamerTest do {:ok, %Pleroma.Activity{data: _data, local: false} = announce} = Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(data) - assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce} + assert_receive {:render_with_user, Pleroma.Web.StreamerView, "update.json", ^announce, _} refute Streamer.filtered_by_user?(user, announce) end @@ -241,7 +322,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user", user, oauth_token) Streamer.stream("user", notify) - assert_receive {:render_with_user, _, _, ^notify} + assert_receive {:render_with_user, _, _, ^notify, _} refute Streamer.filtered_by_user?(user, notify) end @@ -253,7 +334,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) Streamer.stream("user:notification", notify) - assert_receive {:render_with_user, _, _, ^notify} + assert_receive {:render_with_user, _, _, ^notify, _} refute Streamer.filtered_by_user?(user, notify) end @@ -274,7 +355,12 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user:pleroma_chat", user, oauth_token) Streamer.stream("user:pleroma_chat", {user, cm_ref}) - text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) + text = + StreamerView.render( + "chat_update.json", + %{chat_message_reference: cm_ref}, + "user:pleroma_chat:#{user.id}" + ) assert text =~ "hey cirno" assert_receive {:text, ^text} @@ -292,7 +378,12 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user", user, oauth_token) Streamer.stream("user", {user, cm_ref}) - text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}) + text = + StreamerView.render( + "chat_update.json", + %{chat_message_reference: cm_ref}, + "user:#{user.id}" + ) assert text =~ "hey cirno" assert_receive {:text, ^text} @@ -313,7 +404,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) Streamer.stream("user:notification", notify) - assert_receive {:render_with_user, _, _, ^notify} + assert_receive {:render_with_user, _, _, ^notify, _} refute Streamer.filtered_by_user?(user, notify) end @@ -359,7 +450,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) {:ok, favorite_activity} = CommonAPI.favorite(user2, activity.id) - assert_receive {:render_with_user, _, "notification.json", notif} + assert_receive {:render_with_user, _, "notification.json", notif, _} assert notif.activity.id == favorite_activity.id refute Streamer.filtered_by_user?(user, notif) end @@ -388,7 +479,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user:notification", user, oauth_token) {:ok, _follower, _followed, follow_activity} = CommonAPI.follow(user2, user) - assert_receive {:render_with_user, _, "notification.json", notif} + assert_receive {:render_with_user, _, "notification.json", notif, _} assert notif.activity.id == follow_activity.id refute Streamer.filtered_by_user?(user, notif) end @@ -442,6 +533,31 @@ defmodule Pleroma.Web.StreamerTest do "state" => "follow_accept" } = Jason.decode!(payload) end + + test "it streams edits in the 'user' stream", %{user: user, token: oauth_token} do + sender = insert(:user) + {:ok, _, _, _} = CommonAPI.follow(user, sender) + + {:ok, activity} = CommonAPI.post(sender, %{status: "hey"}) + + Streamer.get_topic_and_add_socket("user", user, oauth_token) + {:ok, edited} = CommonAPI.update(sender, activity, %{status: "mew mew"}) + create = Pleroma.Activity.get_create_by_object_ap_id_with_object(activity.object.data["id"]) + + assert_receive {:render_with_user, _, "status_update.json", ^create, _} + refute Streamer.filtered_by_user?(user, edited) + end + + test "it streams own edits in the 'user' stream", %{user: user, token: oauth_token} do + {:ok, activity} = CommonAPI.post(user, %{status: "hey"}) + + Streamer.get_topic_and_add_socket("user", user, oauth_token) + {:ok, edited} = CommonAPI.update(user, activity, %{status: "mew mew"}) + create = Pleroma.Activity.get_create_by_object_ap_id_with_object(activity.object.data["id"]) + + assert_receive {:render_with_user, _, "status_update.json", ^create, _} + refute Streamer.filtered_by_user?(user, edited) + end end describe "public streams" do @@ -452,7 +568,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("public", user, oauth_token) {:ok, activity} = CommonAPI.post(other_user, %{status: "Test"}) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} refute Streamer.filtered_by_user?(other_user, activity) end @@ -484,6 +600,54 @@ defmodule Pleroma.Web.StreamerTest do assert_receive {:text, event} assert %{"event" => "delete", "payload" => ^activity_id} = Jason.decode!(event) end + + test "it streams edits in the 'public' stream" do + sender = insert(:user) + + Streamer.get_topic_and_add_socket("public", nil, nil) + {:ok, activity} = CommonAPI.post(sender, %{status: "hey"}) + assert_receive {:text, _} + + {:ok, edited} = CommonAPI.update(sender, activity, %{status: "mew mew"}) + + edited = Pleroma.Activity.normalize(edited) + + %{id: activity_id} = Pleroma.Activity.get_create_by_object_ap_id(edited.object.data["id"]) + + assert_receive {:text, event} + assert %{"event" => "status.update", "payload" => payload} = Jason.decode!(event) + assert %{"id" => ^activity_id} = Jason.decode!(payload) + refute Streamer.filtered_by_user?(sender, edited) + end + + test "it streams multiple edits in the 'public' stream correctly" do + sender = insert(:user) + + Streamer.get_topic_and_add_socket("public", nil, nil) + {:ok, activity} = CommonAPI.post(sender, %{status: "hey"}) + assert_receive {:text, _} + + {:ok, edited} = CommonAPI.update(sender, activity, %{status: "mew mew"}) + + edited = Pleroma.Activity.normalize(edited) + + %{id: activity_id} = Pleroma.Activity.get_create_by_object_ap_id(edited.object.data["id"]) + + assert_receive {:text, event} + assert %{"event" => "status.update", "payload" => payload} = Jason.decode!(event) + assert %{"id" => ^activity_id} = Jason.decode!(payload) + refute Streamer.filtered_by_user?(sender, edited) + + {:ok, edited} = CommonAPI.update(sender, activity, %{status: "mew mew 2"}) + + edited = Pleroma.Activity.normalize(edited) + + %{id: activity_id} = Pleroma.Activity.get_create_by_object_ap_id(edited.object.data["id"]) + assert_receive {:text, event} + assert %{"event" => "status.update", "payload" => payload} = Jason.decode!(event) + assert %{"id" => ^activity_id, "content" => "mew mew 2"} = Jason.decode!(payload) + refute Streamer.filtered_by_user?(sender, edited) + end end describe "thread_containment/2" do @@ -504,7 +668,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("public", user, oauth_token) Streamer.stream("public", activity) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} assert Streamer.filtered_by_user?(user, activity) end @@ -526,7 +690,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("public", user, oauth_token) Streamer.stream("public", activity) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} refute Streamer.filtered_by_user?(user, activity) end @@ -549,7 +713,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("public", user, oauth_token) Streamer.stream("public", activity) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} refute Streamer.filtered_by_user?(user, activity) end end @@ -563,7 +727,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("public", user, oauth_token) {:ok, activity} = CommonAPI.post(blocked_user, %{status: "Test"}) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} assert Streamer.filtered_by_user?(user, activity) end @@ -580,17 +744,17 @@ defmodule Pleroma.Web.StreamerTest do {:ok, activity_one} = CommonAPI.post(friend, %{status: "hey! @#{blockee.nickname}"}) - assert_receive {:render_with_user, _, _, ^activity_one} + assert_receive {:render_with_user, _, _, ^activity_one, _} assert Streamer.filtered_by_user?(blocker, activity_one) {:ok, activity_two} = CommonAPI.post(blockee, %{status: "hey! @#{friend.nickname}"}) - assert_receive {:render_with_user, _, _, ^activity_two} + assert_receive {:render_with_user, _, _, ^activity_two, _} assert Streamer.filtered_by_user?(blocker, activity_two) {:ok, activity_three} = CommonAPI.post(blockee, %{status: "hey! @#{blocker.nickname}"}) - assert_receive {:render_with_user, _, _, ^activity_three} + assert_receive {:render_with_user, _, _, ^activity_three, _} assert Streamer.filtered_by_user?(blocker, activity_three) end end @@ -651,7 +815,7 @@ defmodule Pleroma.Web.StreamerTest do visibility: "private" }) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} refute Streamer.filtered_by_user?(user_a, activity) end end @@ -669,7 +833,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user", user1, user1_token) {:ok, announce_activity} = CommonAPI.repeat(create_activity.id, user2) - assert_receive {:render_with_user, _, _, ^announce_activity} + assert_receive {:render_with_user, _, _, ^announce_activity, _} assert Streamer.filtered_by_user?(user1, announce_activity) end @@ -685,7 +849,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user", user1, user1_token) {:ok, _announce_activity} = CommonAPI.repeat(create_activity.id, user2) - assert_receive {:render_with_user, _, "notification.json", notif} + assert_receive {:render_with_user, _, "notification.json", notif, _} assert Streamer.filtered_by_user?(user1, notif) end @@ -701,7 +865,7 @@ defmodule Pleroma.Web.StreamerTest do Streamer.get_topic_and_add_socket("user", user1, user1_token) {:ok, _favorite_activity} = CommonAPI.favorite(user2, create_activity.id) - assert_receive {:render_with_user, _, "notification.json", notif} + assert_receive {:render_with_user, _, "notification.json", notif, _} refute Streamer.filtered_by_user?(user1, notif) end end @@ -716,7 +880,7 @@ defmodule Pleroma.Web.StreamerTest do {:ok, activity} = CommonAPI.post(user, %{status: "super hot take"}) {:ok, _} = CommonAPI.add_mute(user2, activity) - assert_receive {:render_with_user, _, _, ^activity} + assert_receive {:render_with_user, _, _, ^activity, _} assert Streamer.filtered_by_user?(user2, activity) end end @@ -758,7 +922,7 @@ defmodule Pleroma.Web.StreamerTest do }) create_activity_id = create_activity.id - assert_receive {:render_with_user, _, _, ^create_activity} + assert_receive {:render_with_user, _, _, ^create_activity, _} assert_receive {:text, received_conversation1} assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1) @@ -793,8 +957,8 @@ defmodule Pleroma.Web.StreamerTest do visibility: "direct" }) - assert_receive {:render_with_user, _, _, ^create_activity} - assert_receive {:render_with_user, _, _, ^create_activity2} + assert_receive {:render_with_user, _, _, ^create_activity, _} + assert_receive {:render_with_user, _, _, ^create_activity2, _} assert_receive {:text, received_conversation1} assert %{"event" => "conversation", "payload" => _} = Jason.decode!(received_conversation1) assert_receive {:text, received_conversation1} @@ -814,4 +978,105 @@ defmodule Pleroma.Web.StreamerTest do assert last_status["id"] == to_string(create_activity.id) end end + + describe "stop streaming if token got revoked" do + setup do + child_proc = fn start, finalize -> + fn -> + start.() + + receive do + {StreamerTest, :ready} -> + assert_receive {:render_with_user, _, "update.json", _, _} + + receive do + {StreamerTest, :revoked} -> finalize.() + end + end + end + end + + starter = fn user, token -> + fn -> Streamer.get_topic_and_add_socket("user", user, token) end + end + + hit = fn -> assert_receive :close end + miss = fn -> refute_receive :close end + + send_all = fn tasks, thing -> Enum.each(tasks, &send(&1.pid, thing)) end + + %{ + child_proc: child_proc, + starter: starter, + hit: hit, + miss: miss, + send_all: send_all + } + end + + test "do not revoke other tokens", %{ + child_proc: child_proc, + starter: starter, + hit: hit, + miss: miss, + send_all: send_all + } do + %{user: user, token: token} = oauth_access(["read"]) + %{token: token2} = oauth_access(["read"], user: user) + %{user: user2, token: user2_token} = oauth_access(["read"]) + + post_user = insert(:user) + CommonAPI.follow(user, post_user) + CommonAPI.follow(user2, post_user) + + tasks = [ + Task.async(child_proc.(starter.(user, token), hit)), + Task.async(child_proc.(starter.(user, token2), miss)), + Task.async(child_proc.(starter.(user2, user2_token), miss)) + ] + + {:ok, _} = + CommonAPI.post(post_user, %{ + status: "hi" + }) + + send_all.(tasks, {StreamerTest, :ready}) + + Pleroma.Web.OAuth.Token.Strategy.Revoke.revoke(token) + + send_all.(tasks, {StreamerTest, :revoked}) + + Enum.each(tasks, &Task.await/1) + end + + test "revoke all streams for this token", %{ + child_proc: child_proc, + starter: starter, + hit: hit, + send_all: send_all + } do + %{user: user, token: token} = oauth_access(["read"]) + + post_user = insert(:user) + CommonAPI.follow(user, post_user) + + tasks = [ + Task.async(child_proc.(starter.(user, token), hit)), + Task.async(child_proc.(starter.(user, token), hit)) + ] + + {:ok, _} = + CommonAPI.post(post_user, %{ + status: "hi" + }) + + send_all.(tasks, {StreamerTest, :ready}) + + Pleroma.Web.OAuth.Token.Strategy.Revoke.revoke(token) + + send_all.(tasks, {StreamerTest, :revoked}) + + Enum.each(tasks, &Task.await/1) + end + end end diff --git a/test/pleroma/web/twitter_api/remote_follow_controller_test.exs b/test/pleroma/web/twitter_api/remote_follow_controller_test.exs index a493dc9f6..c6ecb53f4 100644 --- a/test/pleroma/web/twitter_api/remote_follow_controller_test.exs +++ b/test/pleroma/web/twitter_api/remote_follow_controller_test.exs @@ -7,12 +7,14 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do alias Pleroma.MFA alias Pleroma.MFA.TOTP + alias Pleroma.UnstubbedConfigMock, as: ConfigMock alias Pleroma.User alias Pleroma.Web.CommonAPI + import Ecto.Query import ExUnit.CaptureLog + import Mox import Pleroma.Factory - import Ecto.Query setup_all do: clear_config([:instance, :federating], true) setup do: clear_config([:user, :deny_follow_blocked]) @@ -135,7 +137,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do |> html_response(200) assert response =~ "Error fetching user" - end) =~ "Object has been deleted" + end) =~ ":not_found" end end @@ -410,4 +412,86 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do assert response =~ "Error following account" end end + + describe "avatar url" do + test "without media proxy" do + clear_config([:media_proxy, :enabled], false) + + user = + insert(:user, %{ + local: false, + avatar: %{"url" => [%{"href" => "https://remote.org/avatar.png"}]} + }) + + avatar_url = Pleroma.Web.TwitterAPI.RemoteFollowView.avatar_url(user) + + assert avatar_url == "https://remote.org/avatar.png" + end + + test "with media proxy" do + clear_config([:media_proxy, :enabled], true) + + ConfigMock + |> stub_with(Pleroma.Test.StaticConfig) + + user = + insert(:user, %{ + local: false, + avatar: %{"url" => [%{"href" => "https://remote.org/avatar.png"}]} + }) + + avatar_url = Pleroma.Web.TwitterAPI.RemoteFollowView.avatar_url(user) + url = Pleroma.Web.Endpoint.url() + + assert String.starts_with?(avatar_url, url) + end + + test "local avatar is not proxied" do + clear_config([:media_proxy, :enabled], true) + + user = + insert(:user, %{ + local: true, + avatar: %{"url" => [%{"href" => "#{Pleroma.Web.Endpoint.url()}/localuser/avatar.png"}]} + }) + + avatar_url = Pleroma.Web.TwitterAPI.RemoteFollowView.avatar_url(user) + + assert avatar_url == "#{Pleroma.Web.Endpoint.url()}/localuser/avatar.png" + end + end + + describe "GET /authorize_interaction - authorize_interaction/2" do + test "redirects to /ostatus_subscribe", %{conn: conn} do + Tesla.Mock.mock(fn + %{method: :get, url: "https://mastodon.social/users/emelie"} -> + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/activity+json"}], + body: File.read!("test/fixtures/tesla_mock/emelie.json") + } + + %{method: :get, url: "https://mastodon.social/users/emelie/collections/featured"} -> + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/activity+json"}], + body: + File.read!("test/fixtures/users_mock/masto_featured.json") + |> String.replace("{{domain}}", "mastodon.social") + |> String.replace("{{nickname}}", "emelie") + } + end) + + conn = + conn + |> get( + remote_follow_path(conn, :authorize_interaction, %{ + uri: "https://mastodon.social/users/emelie" + }) + ) + + assert redirected_to(conn) == + remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}) + end + end end diff --git a/test/pleroma/web/twitter_api/util_controller_test.exs b/test/pleroma/web/twitter_api/util_controller_test.exs index c795fd501..d06ae71aa 100644 --- a/test/pleroma/web/twitter_api/util_controller_test.exs +++ b/test/pleroma/web/twitter_api/util_controller_test.exs @@ -106,7 +106,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do |> get("/api/pleroma/healthcheck") |> json_response_and_validate_schema(503) - assert response == %{} + assert response == %{"error" => "Healthcheck disabled"} end test "returns 200 when healthcheck enabled and all ok", %{conn: conn} do @@ -233,6 +233,102 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do end end + describe "POST /main/ostatus - remote_subscribe/2 - with statuses" do + setup do: clear_config([:instance, :federating], true) + + test "renders subscribe form", %{conn: conn} do + user = insert(:user) + status = insert(:note_activity, %{user: user}) + status_id = status.id + + assert is_binary(status_id) + + response = + conn + |> post("/main/ostatus", %{"status_id" => status_id, "profile" => ""}) + |> response(:ok) + + refute response =~ "Could not find status" + assert response =~ "Interacting with" + end + + test "renders subscribe form with error when status not found", %{conn: conn} do + response = + conn + |> post("/main/ostatus", %{"status_id" => "somerandomid", "profile" => ""}) + |> response(:ok) + + assert response =~ "Could not find status" + refute response =~ "Interacting with" + end + + test "it redirect to webfinger url", %{conn: conn} do + user = insert(:user) + status = insert(:note_activity, %{user: user}) + status_id = status.id + status_ap_id = status.data["object"] + + assert is_binary(status_id) + assert is_binary(status_ap_id) + + user2 = insert(:user, ap_id: "shp@social.heldscal.la") + + conn = + conn + |> post("/main/ostatus", %{ + "status" => %{"status_id" => status_id, "profile" => user2.ap_id} + }) + + assert redirected_to(conn) == + "https://social.heldscal.la/main/ostatussub?profile=#{status_ap_id}" + end + + test "it renders form with error when status not found", %{conn: conn} do + user2 = insert(:user, ap_id: "shp@social.heldscal.la") + + response = + conn + |> post("/main/ostatus", %{ + "status" => %{"status_id" => "somerandomid", "profile" => user2.ap_id} + }) + |> response(:ok) + + assert response =~ "Something went wrong." + end + end + + describe "GET /main/ostatus - show_subscribe_form/2" do + setup do: clear_config([:instance, :federating], true) + + test "it works with users", %{conn: conn} do + user = insert(:user) + + response = + conn + |> get("/main/ostatus", %{"nickname" => user.nickname}) + |> response(:ok) + + refute response =~ "Could not find user" + assert response =~ "Remotely follow #{user.nickname}" + end + + test "it works with statuses", %{conn: conn} do + user = insert(:user) + status = insert(:note_activity, %{user: user}) + status_id = status.id + + assert is_binary(status_id) + + response = + conn + |> get("/main/ostatus", %{"status_id" => status_id}) + |> response(:ok) + + refute response =~ "Could not find status" + assert response =~ "Interacting with" + end + end + test "it returns new captcha", %{conn: conn} do with_mock Pleroma.Captcha, new: fn -> "test_captcha" end do @@ -516,4 +612,371 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do assert user.password_hash == nil end end + + describe "POST /api/pleroma/move_account" do + setup do: oauth_access(["write:accounts"]) + + test "without permissions", %{conn: conn} do + target_user = insert(:user) + target_nick = target_user |> User.full_nickname() + + conn = + conn + |> assign(:token, nil) + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/move_account", %{ + "password" => "hi", + "target_account" => target_nick + }) + + assert json_response_and_validate_schema(conn, 403) == %{ + "error" => "Insufficient permissions: write:accounts." + } + end + + test "with proper permissions and invalid password", %{conn: conn} do + target_user = insert(:user) + target_nick = target_user |> User.full_nickname() + + conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/move_account", %{ + "password" => "hi", + "target_account" => target_nick + }) + + assert json_response_and_validate_schema(conn, 200) == %{"error" => "Invalid password."} + end + + test "with proper permissions, valid password and target account does not alias it", + %{ + conn: conn + } do + target_user = insert(:user) + target_nick = target_user |> User.full_nickname() + + conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/move_account", %{ + "password" => "test", + "target_account" => target_nick + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "error" => "Target account must have the origin in `alsoKnownAs`" + } + end + + test "with proper permissions, valid password and target account does not exist", + %{ + conn: conn + } do + target_nick = "not_found@mastodon.social" + + conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/move_account", %{ + "password" => "test", + "target_account" => target_nick + }) + + assert json_response_and_validate_schema(conn, 404) == %{ + "error" => "Target account not found." + } + end + + test "with proper permissions, valid password, remote target account aliases it and local cache does not exist", + %{} do + user = insert(:user, ap_id: "https://lm.kazv.moe/users/testuser") + %{user: _user, conn: conn} = oauth_access(["write:accounts"], user: user) + + target_nick = "mewmew@lm.kazv.moe" + + conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/move_account", %{ + "password" => "test", + "target_account" => target_nick + }) + + assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"} + end + + test "with proper permissions, valid password, remote target account aliases it and local cache does not aliases it", + %{} do + user = insert(:user, ap_id: "https://lm.kazv.moe/users/testuser") + %{user: _user, conn: conn} = oauth_access(["write:accounts"], user: user) + + target_user = + insert( + :user, + ap_id: "https://lm.kazv.moe/users/mewmew", + nickname: "mewmew@lm.kazv.moe", + local: false + ) + + target_nick = target_user |> User.full_nickname() + + conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/move_account", %{ + "password" => "test", + "target_account" => target_nick + }) + + assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"} + end + + test "with proper permissions, valid password, remote target account does not aliases it and local cache aliases it", + %{ + user: user, + conn: conn + } do + target_user = + insert( + :user, + ap_id: "https://lm.kazv.moe/users/mewmew", + nickname: "mewmew@lm.kazv.moe", + local: false, + also_known_as: [user.ap_id] + ) + + target_nick = target_user |> User.full_nickname() + + conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/move_account", %{ + "password" => "test", + "target_account" => target_nick + }) + + assert json_response_and_validate_schema(conn, 200) == %{ + "error" => "Target account must have the origin in `alsoKnownAs`" + } + end + + test "with proper permissions, valid password and target account aliases it", %{ + conn: conn, + user: user + } do + target_user = insert(:user, also_known_as: [user.ap_id]) + target_nick = target_user |> User.full_nickname() + follower = insert(:user) + + User.follow(follower, user) + + assert User.following?(follower, user) + + conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post( + "/api/pleroma/move_account", + %{ + password: "test", + target_account: target_nick + } + ) + + assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"} + + params = %{ + "op" => "move_following", + "origin_id" => user.id, + "target_id" => target_user.id + } + + assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params) + + Pleroma.Workers.BackgroundWorker.perform(%Oban.Job{args: params}) + + refute User.following?(follower, user) + assert User.following?(follower, target_user) + end + + test "prefix nickname by @ should work", %{ + conn: conn, + user: user + } do + target_user = insert(:user, also_known_as: [user.ap_id]) + target_nick = target_user |> User.full_nickname() + follower = insert(:user) + + User.follow(follower, user) + + assert User.following?(follower, user) + + conn = + conn + |> put_req_header("content-type", "multipart/form-data") + |> post( + "/api/pleroma/move_account", + %{ + password: "test", + target_account: "@" <> target_nick + } + ) + + assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"} + + params = %{ + "op" => "move_following", + "origin_id" => user.id, + "target_id" => target_user.id + } + + assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params) + + Pleroma.Workers.BackgroundWorker.perform(%Oban.Job{args: params}) + + refute User.following?(follower, user) + assert User.following?(follower, target_user) + end + end + + describe "GET /api/pleroma/aliases" do + setup do: oauth_access(["read:accounts"]) + + test "without permissions", %{conn: conn} do + conn = + conn + |> assign(:token, nil) + |> get("/api/pleroma/aliases") + + assert json_response_and_validate_schema(conn, 403) == %{ + "error" => "Insufficient permissions: read:accounts." + } + end + + test "with permissions", %{ + conn: conn + } do + assert %{"aliases" => []} = + conn + |> get("/api/pleroma/aliases") + |> json_response_and_validate_schema(200) + end + + test "with permissions and aliases", %{} do + user = insert(:user) + user2 = insert(:user) + + assert {:ok, user} = user |> User.add_alias(user2) + + %{user: _user, conn: conn} = oauth_access(["read:accounts"], user: user) + + assert %{"aliases" => aliases} = + conn + |> get("/api/pleroma/aliases") + |> json_response_and_validate_schema(200) + + assert aliases == [user2 |> User.full_nickname()] + end + end + + describe "PUT /api/pleroma/aliases" do + setup do: oauth_access(["write:accounts"]) + + test "without permissions", %{conn: conn} do + conn = + conn + |> assign(:token, nil) + |> put_req_header("content-type", "application/json") + |> put("/api/pleroma/aliases", %{alias: "none"}) + + assert json_response_and_validate_schema(conn, 403) == %{ + "error" => "Insufficient permissions: write:accounts." + } + end + + test "with permissions, no alias param", %{ + conn: conn + } do + conn = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/pleroma/aliases", %{}) + + assert %{"error" => "Missing field: alias."} = json_response_and_validate_schema(conn, 400) + end + + test "with permissions, with alias param", %{ + conn: conn + } do + user2 = insert(:user) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/pleroma/aliases", %{alias: user2 |> User.full_nickname()}) + + assert json_response_and_validate_schema(conn, 200) == %{ + "status" => "success" + } + end + end + + describe "DELETE /api/pleroma/aliases" do + setup do + alias_user = insert(:user) + non_alias_user = insert(:user) + user = insert(:user, also_known_as: [alias_user.ap_id]) + + oauth_access(["write:accounts"], user: user) + |> Map.put(:alias_user, alias_user) + |> Map.put(:non_alias_user, non_alias_user) + end + + test "without permissions", %{conn: conn} do + conn = + conn + |> assign(:token, nil) + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/aliases", %{alias: "none"}) + + assert json_response_and_validate_schema(conn, 403) == %{ + "error" => "Insufficient permissions: write:accounts." + } + end + + test "with permissions, no alias param", %{conn: conn} do + conn = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/aliases", %{}) + + assert %{"error" => "Missing field: alias."} = json_response_and_validate_schema(conn, 400) + end + + test "with permissions, account does not have such alias", %{ + conn: conn, + non_alias_user: non_alias_user + } do + conn = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/aliases", %{alias: non_alias_user |> User.full_nickname()}) + + assert %{"error" => "Account has no such alias."} = + json_response_and_validate_schema(conn, 404) + end + + test "with permissions, account does have such alias", %{ + conn: conn, + alias_user: alias_user + } do + conn = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/pleroma/aliases", %{alias: alias_user |> User.full_nickname()}) + + assert %{"status" => "success"} = json_response_and_validate_schema(conn, 200) + end + end end diff --git a/test/pleroma/web/web_finger/web_finger_controller_test.exs b/test/pleroma/web/web_finger/web_finger_controller_test.exs index b5be28e67..80e072163 100644 --- a/test/pleroma/web/web_finger/web_finger_controller_test.exs +++ b/test/pleroma/web/web_finger/web_finger_controller_test.exs @@ -23,8 +23,15 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do assert response.status == 200 - assert response.resp_body == - ~s(<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" template="#{Pleroma.Web.Endpoint.url()}/.well-known/webfinger?resource={uri}" type="application/xrd+xml" /></XRD>) + response_xml = + response.resp_body + |> Floki.parse_document!(html_parser: Floki.HTMLParser.Mochiweb, attributes_as_maps: true) + + expected_xml = + ~s(<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" template="#{Pleroma.Web.Endpoint.url()}/.well-known/webfinger?resource={uri}" type="application/xrd+xml" /></XRD>) + |> Floki.parse_document!(html_parser: Floki.HTMLParser.Mochiweb, attributes_as_maps: true) + + assert match?(^response_xml, expected_xml) end test "Webfinger JRD" do @@ -48,6 +55,23 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do ] end + test "reach user on tld, while pleroma is running on subdomain" do + clear_config([Pleroma.Web.Endpoint, :url, :host], "sub.example.com") + + clear_config([Pleroma.Web.WebFinger, :domain], "example.com") + + user = insert(:user, ap_id: "https://sub.example.com/users/bobby", nickname: "bobby") + + response = + build_conn() + |> put_req_header("accept", "application/jrd+json") + |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@example.com") + |> json_response(200) + + assert response["subject"] == "acct:#{user.nickname}@example.com" + assert response["aliases"] == ["https://sub.example.com/users/#{user.nickname}"] + end + test "it returns 404 when user isn't found (JSON)" do result = build_conn() diff --git a/test/pleroma/web/web_finger_test.exs b/test/pleroma/web/web_finger_test.exs index 1cc6ae675..8a550a6ba 100644 --- a/test/pleroma/web/web_finger_test.exs +++ b/test/pleroma/web/web_finger_test.exs @@ -47,7 +47,7 @@ defmodule Pleroma.Web.WebFingerTest do test "returns error when there is no content-type header" do Tesla.Mock.mock(fn - %{url: "http://social.heldscal.la/.well-known/host-meta"} -> + %{url: "https://social.heldscal.la/.well-known/host-meta"} -> {:ok, %Tesla.Env{ status: 200, @@ -76,15 +76,6 @@ defmodule Pleroma.Web.WebFingerTest do {:ok, _data} = WebFinger.finger(user) end - test "returns the ActivityPub actor URI and subscribe address for an ActivityPub user with the ld+json mimetype" do - user = "kaniini@gerzilla.de" - - {:ok, data} = WebFinger.finger(user) - - assert data["ap_id"] == "https://gerzilla.de/channel/kaniini" - assert data["subscribe_address"] == "https://gerzilla.de/follow?f=&url={uri}" - end - test "it work for AP-only user" do user = "kpherox@mstdn.jp" @@ -99,12 +90,6 @@ defmodule Pleroma.Web.WebFingerTest do assert data["subscribe_address"] == "https://mstdn.jp/authorize_interaction?acct={uri}" end - test "it works for friendica" do - user = "lain@squeet.me" - - {:ok, _data} = WebFinger.finger(user) - end - test "it gets the xrd endpoint" do {:ok, template} = WebFinger.find_lrdd_template("social.heldscal.la") @@ -120,7 +105,7 @@ defmodule Pleroma.Web.WebFingerTest do test "it gets the xrd endpoint for statusnet" do {:ok, template} = WebFinger.find_lrdd_template("status.alpicola.com") - assert template == "http://status.alpicola.com/main/xrd?uri={uri}" + assert template == "https://status.alpicola.com/main/xrd?uri={uri}" end test "it works with idna domains as nickname" do @@ -147,7 +132,7 @@ defmodule Pleroma.Web.WebFingerTest do headers: [{"content-type", "application/jrd+json"}] }} - %{url: "http://mastodon.social/.well-known/host-meta"} -> + %{url: "https://mastodon.social/.well-known/host-meta"} -> {:ok, %Tesla.Env{ status: 200, @@ -170,7 +155,7 @@ defmodule Pleroma.Web.WebFingerTest do headers: [{"content-type", "application/xrd+xml"}] }} - %{url: "http://pawoo.net/.well-known/host-meta"} -> + %{url: "https://pawoo.net/.well-known/host-meta"} -> {:ok, %Tesla.Env{ status: 200, @@ -180,5 +165,67 @@ defmodule Pleroma.Web.WebFingerTest do {:ok, _data} = WebFinger.finger("pekorino@pawoo.net") end + + test "refuses to process XML remote entities" do + Tesla.Mock.mock(fn + %{ + url: "https://pawoo.net/.well-known/webfinger?resource=acct:pekorino@pawoo.net" + } -> + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/xml_external_entities.xml"), + headers: [{"content-type", "application/xrd+xml"}] + }} + + %{url: "https://pawoo.net/.well-known/host-meta"} -> + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/pawoo.net_host_meta") + }} + end) + + assert :error = WebFinger.finger("pekorino@pawoo.net") + end + + test "prevents spoofing" do + Tesla.Mock.mock(fn + %{ + url: "https://gleasonator.com/.well-known/webfinger?resource=acct:alex@gleasonator.com" + } -> + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/webfinger_spoof.json"), + headers: [{"content-type", "application/jrd+json"}] + }} + + %{url: "https://gleasonator.com/.well-known/host-meta"} -> + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/gleasonator.com_host_meta") + }} + end) + + {:error, _data} = WebFinger.finger("alex@gleasonator.com") + end + end + + @tag capture_log: true + test "prevents forgeries" do + Tesla.Mock.mock(fn + %{url: "https://fba.ryona.agency/.well-known/webfinger?resource=acct:graf@fba.ryona.agency"} -> + fake_webfinger = + File.read!("test/fixtures/webfinger/graf-imposter-webfinger.json") |> Jason.decode!() + + Tesla.Mock.json(fake_webfinger) + + %{url: "https://fba.ryona.agency/.well-known/host-meta"} -> + {:ok, %Tesla.Env{status: 404}} + end) + + assert {:error, _} = WebFinger.finger("graf@fba.ryona.agency") end end diff --git a/test/pleroma/web/xml_test.exs b/test/pleroma/web/xml_test.exs new file mode 100644 index 000000000..49306430b --- /dev/null +++ b/test/pleroma/web/xml_test.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Web.XMLTest do + use Pleroma.DataCase, async: true + + alias Pleroma.Web.XML + + test "refuses to parse any entities from XML" do + data = File.read!("test/fixtures/xml_billion_laughs.xml") + assert(:error == XML.parse_document(data)) + end + + test "refuses to load external entities from XML" do + data = File.read!("test/fixtures/xml_external_entities.xml") + assert(:error == XML.parse_document(data)) + end +end diff --git a/test/pleroma/workers/cron/digest_emails_worker_test.exs b/test/pleroma/workers/cron/digest_emails_worker_test.exs index 851f4d63a..e0bdf303e 100644 --- a/test/pleroma/workers/cron/digest_emails_worker_test.exs +++ b/test/pleroma/workers/cron/digest_emails_worker_test.exs @@ -14,6 +14,11 @@ defmodule Pleroma.Workers.Cron.DigestEmailsWorkerTest do setup do: clear_config([:email_notifications, :digest]) setup do + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + :ok + end + + setup do clear_config([:email_notifications, :digest], %{ active: true, inactivity_threshold: 7, diff --git a/test/pleroma/workers/cron/new_users_digest_worker_test.exs b/test/pleroma/workers/cron/new_users_digest_worker_test.exs index 84914876c..0e4234cc8 100644 --- a/test/pleroma/workers/cron/new_users_digest_worker_test.exs +++ b/test/pleroma/workers/cron/new_users_digest_worker_test.exs @@ -10,6 +10,11 @@ defmodule Pleroma.Workers.Cron.NewUsersDigestWorkerTest do alias Pleroma.Web.CommonAPI alias Pleroma.Workers.Cron.NewUsersDigestWorker + setup do + Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config) + :ok + end + test "it sends new users digest emails" do yesterday = NaiveDateTime.utc_now() |> Timex.shift(days: -1) admin = insert(:user, %{is_admin: true}) diff --git a/test/pleroma/workers/publisher_worker_test.exs b/test/pleroma/workers/publisher_worker_test.exs new file mode 100644 index 000000000..13372bf49 --- /dev/null +++ b/test/pleroma/workers/publisher_worker_test.exs @@ -0,0 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.PublisherWorkerTest do + use Pleroma.DataCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + import Pleroma.Factory + + alias Pleroma.Object + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Builder + alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Federator + + describe "Oban job priority:" do + setup do + user = insert(:user) + + {:ok, post} = CommonAPI.post(user, %{status: "Regrettable post"}) + object = Object.normalize(post, fetch: false) + {:ok, delete_data, _meta} = Builder.delete(user, object.data["id"]) + {:ok, delete, _meta} = ActivityPub.persist(delete_data, local: true) + + %{ + post: post, + delete: delete + } + end + + test "Deletions are lower priority", %{delete: delete} do + assert {:ok, %Oban.Job{priority: 3}} = Federator.publish(delete) + end + + test "Creates are normal priority", %{post: post} do + assert {:ok, %Oban.Job{priority: 0}} = Federator.publish(post) + end + end +end diff --git a/test/pleroma/workers/purge_expired_token_test.exs b/test/pleroma/workers/purge_expired_token_test.exs index d891eb8bb..add572bb8 100644 --- a/test/pleroma/workers/purge_expired_token_test.exs +++ b/test/pleroma/workers/purge_expired_token_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Workers.PurgeExpiredTokenTest do - use Pleroma.DataCase, async: true + use Pleroma.DataCase use Oban.Testing, repo: Pleroma.Repo import Pleroma.Factory diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs new file mode 100644 index 000000000..b9b6d6af2 --- /dev/null +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -0,0 +1,52 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.ReceiverWorkerTest do + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + + import Mock + import Pleroma.Factory + + alias Pleroma.Workers.ReceiverWorker + + test "it does not retry MRF reject" do + params = insert(:note).data + + with_mock Pleroma.Web.ActivityPub.Transmogrifier, + handle_incoming: fn _ -> {:reject, "MRF"} end do + assert {:cancel, "MRF"} = + ReceiverWorker.perform(%Oban.Job{ + args: %{"op" => "incoming_ap_doc", "params" => params} + }) + end + end + + test "it does not retry ObjectValidator reject" do + params = + insert(:note_activity).data + |> Map.put("id", Pleroma.Web.ActivityPub.Utils.generate_activity_id()) + |> Map.put("object", %{ + "type" => "Note", + "id" => Pleroma.Web.ActivityPub.Utils.generate_object_id() + }) + + with_mock Pleroma.Web.ActivityPub.ObjectValidator, [:passthrough], + validate: fn _, _ -> {:error, %Ecto.Changeset{}} end do + assert {:cancel, {:error, %Ecto.Changeset{}}} = + ReceiverWorker.perform(%Oban.Job{ + args: %{"op" => "incoming_ap_doc", "params" => params} + }) + end + end + + test "it does not retry duplicates" do + params = insert(:note_activity).data + + assert {:cancel, :already_present} = + ReceiverWorker.perform(%Oban.Job{ + args: %{"op" => "incoming_ap_doc", "params" => params} + }) + end +end diff --git a/test/pleroma/workers/remote_fetcher_worker_test.exs b/test/pleroma/workers/remote_fetcher_worker_test.exs new file mode 100644 index 000000000..c30e773d4 --- /dev/null +++ b/test/pleroma/workers/remote_fetcher_worker_test.exs @@ -0,0 +1,69 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.RemoteFetcherWorkerTest do + use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo + + alias Pleroma.Workers.RemoteFetcherWorker + + @deleted_object_one "https://deleted-404.example.com/" + @deleted_object_two "https://deleted-410.example.com/" + @unauthorized_object "https://unauthorized.example.com/" + @depth_object "https://depth.example.com/" + + describe "RemoteFetcherWorker" do + setup do + Tesla.Mock.mock(fn + %{method: :get, url: @deleted_object_one} -> + %Tesla.Env{ + status: 404 + } + + %{method: :get, url: @deleted_object_two} -> + %Tesla.Env{ + status: 410 + } + + %{method: :get, url: @unauthorized_object} -> + %Tesla.Env{ + status: 403 + } + + %{method: :get, url: @depth_object} -> + %Tesla.Env{ + status: 200 + } + end) + end + + test "does not requeue a deleted object" do + assert {:discard, _} = + RemoteFetcherWorker.perform(%Oban.Job{ + args: %{"op" => "fetch_remote", "id" => @deleted_object_one} + }) + + assert {:discard, _} = + RemoteFetcherWorker.perform(%Oban.Job{ + args: %{"op" => "fetch_remote", "id" => @deleted_object_two} + }) + end + + test "does not requeue an unauthorized object" do + assert {:discard, _} = + RemoteFetcherWorker.perform(%Oban.Job{ + args: %{"op" => "fetch_remote", "id" => @unauthorized_object} + }) + end + + test "does not requeue an object that exceeded depth" do + clear_config([:instance, :federation_incoming_replies_max_depth], 0) + + assert {:discard, _} = + RemoteFetcherWorker.perform(%Oban.Job{ + args: %{"op" => "fetch_remote", "id" => @depth_object, "depth" => 1} + }) + end + end +end diff --git a/test/support/cachex_proxy.ex b/test/support/cachex_proxy.ex index 83ae5610f..8f27986a9 100644 --- a/test/support/cachex_proxy.ex +++ b/test/support/cachex_proxy.ex @@ -27,9 +27,15 @@ defmodule Pleroma.CachexProxy do defdelegate fetch!(cache, key, func), to: Cachex @impl true + defdelegate fetch(cache, key, func), to: Cachex + + @impl true defdelegate expire_at(cache, str, num), to: Cachex @impl true + defdelegate expire(cache, str, num), to: Cachex + + @impl true defdelegate exists?(cache, key), to: Cachex @impl true diff --git a/test/support/cluster.ex b/test/support/cluster.ex index 1c923fb0c..a0ec91168 100644 --- a/test/support/cluster.ex +++ b/test/support/cluster.ex @@ -127,7 +127,10 @@ defmodule Pleroma.Cluster do defp start_slave({node_host, override_configs}) do log(node_host, "booting federated VM") - {:ok, node} = :slave.start(~c"127.0.0.1", node_name(node_host), vm_args()) + + {:ok, node} = + do_start_slave(%{host: "127.0.0.1", name: node_name(node_host), args: vm_args()}) + add_code_paths(node) load_apps_and_transfer_configuration(node, override_configs) ensure_apps_started(node) @@ -219,4 +222,14 @@ defmodule Pleroma.Cluster do |> Enum.at(0) |> String.to_atom() end + + defp do_start_slave(%{host: host, name: name, args: args} = opts) do + peer_module = Application.get_env(__MODULE__, :peer_module) + + if peer_module == :peer do + peer_module.start(opts) + else + peer_module.start(host, name, args) + end + end end diff --git a/test/support/data_case.ex b/test/support/data_case.ex index 3c9cab061..14403f0b8 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -115,6 +115,7 @@ defmodule Pleroma.DataCase do Mox.stub_with(Pleroma.Web.ActivityPub.ActivityPubMock, Pleroma.Web.ActivityPub.ActivityPub) Mox.stub_with(Pleroma.Web.FederatorMock, Pleroma.Web.Federator) Mox.stub_with(Pleroma.ConfigMock, Pleroma.Config) + Mox.stub_with(Pleroma.StaticStubbedConfigMock, Pleroma.Test.StaticConfig) end def ensure_local_uploader(context) do diff --git a/test/support/factory.ex b/test/support/factory.ex index efbf3df2e..20bc5162e 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -10,6 +10,15 @@ defmodule Pleroma.Factory do alias Pleroma.Object alias Pleroma.User + @rsa_keys [ + "test/fixtures/rsa_keys/key_1.pem", + "test/fixtures/rsa_keys/key_2.pem", + "test/fixtures/rsa_keys/key_3.pem", + "test/fixtures/rsa_keys/key_4.pem", + "test/fixtures/rsa_keys/key_5.pem" + ] + |> Enum.map(&File.read!/1) + def participation_factory do conversation = insert(:conversation) user = insert(:user) @@ -28,6 +37,8 @@ defmodule Pleroma.Factory do end def user_factory(attrs \\ %{}) do + pem = Enum.random(@rsa_keys) + user = %User{ name: sequence(:name, &"Test テスト User #{&1}"), email: sequence(:email, &"user#{&1}@example.com"), @@ -39,7 +50,7 @@ defmodule Pleroma.Factory do last_refreshed_at: NaiveDateTime.utc_now(), notification_settings: %Pleroma.User.NotificationSetting{}, multi_factor_authentication_settings: %Pleroma.MFA.Settings{}, - ap_enabled: true + keys: pem } urls = @@ -111,6 +122,18 @@ defmodule Pleroma.Factory do } end + def attachment_factory(attrs \\ %{}) do + user = attrs[:user] || insert(:user) + + data = + attachment_data(user.ap_id, nil) + |> Map.put("id", Pleroma.Web.ActivityPub.Utils.generate_object_id()) + + %Pleroma.Object{ + data: merge_attributes(data, Map.get(attrs, :data, %{})) + } + end + def attachment_note_factory(attrs \\ %{}) do user = attrs[:user] || insert(:user) {length, attrs} = Map.pop(attrs, :length, 1) @@ -189,7 +212,7 @@ defmodule Pleroma.Factory do end def direct_note_factory do - user2 = insert(:user) + user2 = insert(:user, local: false, inbox: "http://example.com/inbox") %Pleroma.Object{data: data} = note_factory() %Pleroma.Object{data: Map.merge(data, %{"to" => [user2.ap_id]})} diff --git a/test/support/helpers.ex b/test/support/helpers.ex index 0bd487f39..7fa6c31a4 100644 --- a/test/support/helpers.ex +++ b/test/support/helpers.ex @@ -10,6 +10,39 @@ defmodule Pleroma.Tests.Helpers do require Logger + @doc "Accepts two URLs/URIs and sorts the query parameters before comparing" + def uri_equal?(a, b) do + a_sorted = uri_query_sort(a) + b_sorted = uri_query_sort(b) + + match?(^a_sorted, b_sorted) + end + + @doc "Accepts a URL/URI and sorts the query parameters" + def uri_query_sort(uri) do + parsed = URI.parse(uri) + + sorted_query = + String.split(parsed.query, "&") + |> Enum.sort() + |> Enum.join("&") + + parsed + |> Map.put(:query, sorted_query) + |> URI.to_string() + end + + @doc "Returns the value of the specified query parameter for the provided URL" + def get_query_parameter(url, param) do + url + |> URI.parse() + |> Map.get(:query) + |> URI.query_decoder() + |> Enum.to_list() + |> Enum.into(%{}, fn {x, y} -> {x, y} end) + |> Map.get(param) + end + defmacro clear_config(config_path) do quote do clear_config(unquote(config_path)) do @@ -41,7 +74,7 @@ defmodule Pleroma.Tests.Helpers do # NOTE: `clear_config([section, key], value)` != `clear_config([section], key: value)` (!) # Displaying a warning to prevent unintentional clearing of all but one keys in section if Keyword.keyword?(temp_setting) and length(temp_setting) == 1 do - Logger.warn( + Logger.warning( "Please change `clear_config([section], key: value)` to `clear_config([section, key], value)`" ) end diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index ad4925a5c..20e410424 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -21,7 +21,7 @@ defmodule HttpRequestMock do else error -> with {:error, message} <- error do - Logger.warn(to_string(message)) + Logger.warning(to_string(message)) end {_, _r} = error @@ -178,7 +178,7 @@ defmodule HttpRequestMock do end def get( - "https://social.heldscal.la/.well-known/webfinger?resource=nonexistant@social.heldscal.la", + "https://social.heldscal.la/.well-known/webfinger?resource=nonexistent@social.heldscal.la", _, _, [{"accept", "application/xrd+xml,application/jrd+json"}] @@ -186,7 +186,7 @@ defmodule HttpRequestMock do {:ok, %Tesla.Env{ status: 200, - body: File.read!("test/fixtures/tesla_mock/nonexistant@social.heldscal.la.xml") + body: File.read!("test/fixtures/tesla_mock/nonexistent@social.heldscal.la.xml") }} end @@ -424,14 +424,6 @@ defmodule HttpRequestMock do {:error, :nxdomain} end - def get("http://osada.macgirvin.com/.well-known/host-meta", _, _, _) do - {:ok, - %Tesla.Env{ - status: 404, - body: "" - }} - end - def get("https://osada.macgirvin.com/.well-known/host-meta", _, _, _) do {:ok, %Tesla.Env{ @@ -725,6 +717,15 @@ defmodule HttpRequestMock do }} end + def get( + "https://mastodon.social/.well-known/webfinger?resource=acct:not_found@mastodon.social", + _, + _, + [{"accept", "application/xrd+xml,application/jrd+json"}] + ) do + {:ok, %Tesla.Env{status: 404}} + end + def get("http://gs.example.org/.well-known/host-meta", _, _, _) do {:ok, %Tesla.Env{ @@ -756,7 +757,7 @@ defmodule HttpRequestMock do {:ok, %Tesla.Env{status: 406, body: ""}} end - def get("http://squeet.me/.well-known/host-meta", _, _, _) do + def get("https://squeet.me/.well-known/host-meta", _, _, _) do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/squeet.me_host_meta")}} end @@ -797,7 +798,7 @@ defmodule HttpRequestMock do {:ok, %Tesla.Env{status: 200, body: "", headers: [{"content-type", "application/jrd+json"}]}} end - def get("http://framatube.org/.well-known/host-meta", _, _, _) do + def get("https://framatube.org/.well-known/host-meta", _, _, _) do {:ok, %Tesla.Env{ status: 200, @@ -806,7 +807,7 @@ defmodule HttpRequestMock do end def get( - "http://framatube.org/main/xrd?uri=acct:framasoft@framatube.org", + "https://framatube.org/main/xrd?uri=acct:framasoft@framatube.org", _, _, [{"accept", "application/xrd+xml,application/jrd+json"}] @@ -841,7 +842,7 @@ defmodule HttpRequestMock do }} end - def get("http://status.alpicola.com/.well-known/host-meta", _, _, _) do + def get("https://status.alpicola.com/.well-known/host-meta", _, _, _) do {:ok, %Tesla.Env{ status: 200, @@ -849,7 +850,7 @@ defmodule HttpRequestMock do }} end - def get("http://macgirvin.com/.well-known/host-meta", _, _, _) do + def get("https://macgirvin.com/.well-known/host-meta", _, _, _) do {:ok, %Tesla.Env{ status: 200, @@ -857,7 +858,7 @@ defmodule HttpRequestMock do }} end - def get("http://gerzilla.de/.well-known/host-meta", _, _, _) do + def get("https://gerzilla.de/.well-known/host-meta", _, _, _) do {:ok, %Tesla.Env{ status: 200, @@ -1058,7 +1059,7 @@ defmodule HttpRequestMock do }} end - def get("http://example.com/malformed", _, _, _) do + def get("https://example.com/malformed", _, _, _) do {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/malformed-data.html")}} end @@ -1075,6 +1076,14 @@ defmodule HttpRequestMock do }} end + def get("https://404.site" <> _, _, _, _) do + {:ok, + %Tesla.Env{ + status: 404, + body: "" + }} + end + def get( "https://zetsubou.xn--q9jyb4c/.well-known/webfinger?resource=acct:lain@zetsubou.xn--q9jyb4c", _, @@ -1124,6 +1133,57 @@ defmodule HttpRequestMock do }} end + def get("http://lm.kazv.moe/.well-known/host-meta", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/lm.kazv.moe_host_meta") + }} + end + + def get("https://lm.kazv.moe/.well-known/host-meta", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/lm.kazv.moe_host_meta") + }} + end + + def get( + "https://lm.kazv.moe/.well-known/webfinger?resource=acct:mewmew@lm.kazv.moe", + _, + _, + [{"accept", "application/xrd+xml,application/jrd+json"}] + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/https___lm.kazv.moe_users_mewmew.xml"), + headers: [{"content-type", "application/xrd+xml"}] + }} + end + + def get("https://lm.kazv.moe/users/mewmew", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/mewmew@lm.kazv.moe.json"), + headers: activitypub_object_headers() + }} + end + + def get("https://lm.kazv.moe/users/mewmew/collections/featured", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: + File.read!("test/fixtures/users_mock/masto_featured.json") + |> String.replace("{{domain}}", "lm.kazv.moe") + |> String.replace("{{nickname}}", "mewmew"), + headers: [{"content-type", "application/activity+json"}] + }} + end + def get("https://info.pleroma.site/activity.json", _, _, [ {"accept", "application/activity+json"} ]) do @@ -1320,6 +1380,15 @@ defmodule HttpRequestMock do }} end + def get("https://misskey.io/users/83ssedkv53", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/aimu@misskey.io.json"), + headers: activitypub_object_headers() + }} + end + def get("https://gleasonator.com/users/macgirvin", _, _, _) do {:ok, %Tesla.Env{ @@ -1341,6 +1410,231 @@ defmodule HttpRequestMock do }} end + def get("https://mk.absturztau.be/users/8ozbzjs3o8", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/mametsuko@mk.absturztau.be.json"), + headers: activitypub_object_headers() + }} + end + + def get("https://p.helene.moe/users/helene", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/helene@p.helene.moe.json"), + headers: activitypub_object_headers() + }} + end + + def get("https://mk.absturztau.be/notes/93e7nm8wqg", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg.json"), + headers: activitypub_object_headers() + }} + end + + def get("https://mk.absturztau.be/notes/93e7nm8wqg/activity", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json"), + headers: activitypub_object_headers() + }} + end + + def get("https://p.helene.moe/objects/fd5910ac-d9dc-412e-8d1d-914b203296c4", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/p.helene.moe-AM7S6vZQmL6pI9TgPY.json"), + headers: activitypub_object_headers() + }} + end + + def get("https://misskey.io/notes/8vs6wxufd0", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json"), + headers: activitypub_object_headers() + }} + end + + def get("https://google.com/", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/google.html")}} + end + + def get("https://yahoo.com/", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/yahoo.html")}} + end + + def get("https://example.com/error", _, _, _), do: {:error, :overload} + + def get("https://example.com/ogp-missing-title", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/rich_media/ogp-missing-title.html") + }} + end + + def get("https://example.com/oembed", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/oembed.html")}} + end + + def get("https://example.com/oembed.json", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/oembed.json")}} + end + + def get("https://example.com/twitter-card", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")}} + end + + def get("https://example.com/non-ogp", _, _, _) do + {:ok, + %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/non_ogp_embed.html")}} + end + + def get("https://example.com/empty", _, _, _) do + {:ok, %Tesla.Env{status: 200, body: "hello"}} + end + + def get("https://friends.grishka.me/posts/54642", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/smithereen_non_anonymous_poll.json"), + headers: activitypub_object_headers() + }} + end + + def get("https://friends.grishka.me/users/1", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/smithereen_user.json"), + headers: activitypub_object_headers() + }} + end + + def get("https://mastodon.example/.well-known/host-meta", _, _, _) do + {:ok, + %Tesla.Env{ + status: 302, + headers: [{"location", "https://sub.mastodon.example/.well-known/host-meta"}] + }} + end + + def get("https://sub.mastodon.example/.well-known/host-meta", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: + "test/fixtures/webfinger/masto-host-meta.xml" + |> File.read!() + |> String.replace("{{domain}}", "sub.mastodon.example") + }} + end + + def get( + "https://sub.mastodon.example/.well-known/webfinger?resource=acct:a@mastodon.example", + _, + _, + _ + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: + "test/fixtures/webfinger/masto-webfinger.json" + |> File.read!() + |> String.replace("{{nickname}}", "a") + |> String.replace("{{domain}}", "mastodon.example") + |> String.replace("{{subdomain}}", "sub.mastodon.example"), + headers: [{"content-type", "application/jrd+json"}] + }} + end + + def get("https://sub.mastodon.example/users/a", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: + "test/fixtures/webfinger/masto-user.json" + |> File.read!() + |> String.replace("{{nickname}}", "a") + |> String.replace("{{domain}}", "sub.mastodon.example"), + headers: [{"content-type", "application/activity+json"}] + }} + end + + def get("https://sub.mastodon.example/users/a/collections/featured", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: + File.read!("test/fixtures/users_mock/masto_featured.json") + |> String.replace("{{domain}}", "sub.mastodon.example") + |> String.replace("{{nickname}}", "a"), + headers: [{"content-type", "application/activity+json"}] + }} + end + + def get("https://pleroma.example/.well-known/host-meta", _, _, _) do + {:ok, + %Tesla.Env{ + status: 302, + headers: [{"location", "https://sub.pleroma.example/.well-known/host-meta"}] + }} + end + + def get("https://sub.pleroma.example/.well-known/host-meta", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: + "test/fixtures/webfinger/pleroma-host-meta.xml" + |> File.read!() + |> String.replace("{{domain}}", "sub.pleroma.example") + }} + end + + def get( + "https://sub.pleroma.example/.well-known/webfinger?resource=acct:a@pleroma.example", + _, + _, + _ + ) do + {:ok, + %Tesla.Env{ + status: 200, + body: + "test/fixtures/webfinger/pleroma-webfinger.json" + |> File.read!() + |> String.replace("{{nickname}}", "a") + |> String.replace("{{domain}}", "pleroma.example") + |> String.replace("{{subdomain}}", "sub.pleroma.example"), + headers: [{"content-type", "application/jrd+json"}] + }} + end + + def get("https://sub.pleroma.example/users/a", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: + "test/fixtures/webfinger/pleroma-user.json" + |> File.read!() + |> String.replace("{{nickname}}", "a") + |> String.replace("{{domain}}", "sub.pleroma.example"), + headers: [{"content-type", "application/activity+json"}] + }} + end + def get(url, query, body, headers) do {:error, "Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"} @@ -1414,14 +1708,41 @@ defmodule HttpRequestMock do # Most of the rich media mocks are missing HEAD requests, so we just return 404. @rich_media_mocks [ + "https://example.com/empty", + "https://example.com/error", + "https://example.com/malformed", + "https://example.com/non-ogp", + "https://example.com/oembed", + "https://example.com/oembed.json", "https://example.com/ogp", "https://example.com/ogp-missing-data", - "https://example.com/twitter-card" + "https://example.com/ogp-missing-title", + "https://example.com/twitter-card", + "https://google.com/", + "https://pleroma.local/notice/9kCP7V", + "https://yahoo.com/" ] + def head(url, _query, _body, _headers) when url in @rich_media_mocks do {:ok, %Tesla.Env{status: 404, body: ""}} end + def head("https://example.com/pdf-file", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + headers: [{"content-length", "1000000"}, {"content-type", "application/pdf"}] + }} + end + + def head("https://example.com/huge-page", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + headers: [{"content-length", "2000001"}, {"content-type", "text/html"}] + }} + end + def head(url, query, body, headers) do {:error, "Mock response not implemented for HEAD #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"} diff --git a/test/support/mocks.ex b/test/support/mocks.ex index d167996bd..d906f0e1d 100644 --- a/test/support/mocks.ex +++ b/test/support/mocks.ex @@ -26,5 +26,11 @@ Mox.defmock(Pleroma.Web.ActivityPub.SideEffectsMock, Mox.defmock(Pleroma.Web.FederatorMock, for: Pleroma.Web.Federator.Publishing) Mox.defmock(Pleroma.ConfigMock, for: Pleroma.Config.Getting) +Mox.defmock(Pleroma.UnstubbedConfigMock, for: Pleroma.Config.Getting) +Mox.defmock(Pleroma.StaticStubbedConfigMock, for: Pleroma.Config.Getting) Mox.defmock(Pleroma.LoggerMock, for: Pleroma.Logging) + +Mox.defmock(Pleroma.User.Backup.ProcessorMock, for: Pleroma.User.Backup.ProcessorAPI) + +Mox.defmock(Pleroma.Uploaders.S3.ExAwsMock, for: Pleroma.Uploaders.S3.ExAwsAPI) diff --git a/test/support/null_cache.ex b/test/support/null_cache.ex index 9f1d45f1d..47c84174e 100644 --- a/test/support/null_cache.ex +++ b/test/support/null_cache.ex @@ -29,6 +29,9 @@ defmodule Pleroma.NullCache do end @impl true + def fetch(_, key, func), do: func.(key) + + @impl true def get_and_update(_, _, func) do func.(nil) end @@ -37,6 +40,9 @@ defmodule Pleroma.NullCache do def expire_at(_, _, _), do: {:ok, true} @impl true + def expire(_, _, _), do: {:ok, true} + + @impl true def exists?(_, _), do: {:ok, false} @impl true diff --git a/test/support/websocket_client.ex b/test/support/websocket_client.ex index d149b324e..7163bbd41 100644 --- a/test/support/websocket_client.ex +++ b/test/support/websocket_client.ex @@ -5,18 +5,17 @@ defmodule Pleroma.Integration.WebsocketClient do # https://github.com/phoenixframework/phoenix/blob/master/test/support/websocket_client.exs + use WebSockex + @doc """ Starts the WebSocket server for given ws URL. Received Socket.Message's are forwarded to the sender pid """ def start_link(sender, url, headers \\ []) do - :crypto.start() - :ssl.start() - - :websocket_client.start_link( - String.to_charlist(url), + WebSockex.start_link( + url, __MODULE__, - [sender], + %{sender: sender}, extra_headers: headers ) end @@ -36,27 +35,32 @@ defmodule Pleroma.Integration.WebsocketClient do end @doc false - def init([sender], _conn_state) do - {:ok, %{sender: sender}} + @impl true + def handle_frame(frame, state) do + send(state.sender, frame) + {:ok, state} end - @doc false - def websocket_handle(frame, _conn_state, state) do - send(state.sender, frame) + @impl true + def handle_disconnect(conn_status, state) do + send(state.sender, {:close, conn_status}) {:ok, state} end @doc false - def websocket_info({:text, msg}, _conn_state, state) do + @impl true + def handle_info({:text, msg}, state) do {:reply, {:text, msg}, state} end - def websocket_info(:close, _conn_state, _state) do + @impl true + def handle_info(:close, _state) do {:close, <<>>, "done"} end @doc false - def websocket_terminate(_reason, _conn_state, _state) do + @impl true + def terminate(_reason, _state) do :ok end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 60a61484f..a117584ae 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -2,8 +2,17 @@ # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only -os_exclude = if :os.type() == {:unix, :darwin}, do: [skip_on_mac: true], else: [] -ExUnit.start(exclude: [:federated, :erratic] ++ os_exclude) +Code.put_compiler_option(:warnings_as_errors, true) + +ExUnit.configure(max_cases: System.schedulers_online()) + +ExUnit.start(exclude: [:federated, :erratic]) + +if match?({:unix, :darwin}, :os.type()) do + excluded = ExUnit.configuration() |> Keyword.get(:exclude, []) + excluded = excluded ++ [:skip_darwin] + ExUnit.configure(exclude: excluded) +end Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual) @@ -16,3 +25,16 @@ ExUnit.after_suite(fn _results -> uploads = Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads], "test/uploads") File.rm_rf!(uploads) end) + +defmodule Pleroma.Test.StaticConfig do + @moduledoc """ + This module provides a Config that is completely static, built at startup time from the environment. It's safe to use in testing as it will not modify any state. + """ + + @behaviour Pleroma.Config.Getting + @config Application.get_all_env(:pleroma) + + def get(path, default \\ nil) do + get_in(@config, path) || default + end +end |