summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/fixtures/ccworld-ap-bridge_note.json1
-rw-r--r--test/fixtures/config/temp.secret.exs2
-rw-r--r--test/fixtures/create-pleroma-reply-to-misskey-thread.json61
-rw-r--r--test/fixtures/custom-emoji-reaction.json28
-rw-r--r--test/fixtures/fep-e232.json31
-rw-r--r--test/fixtures/hubzilla-actor.json1
-rw-r--r--test/fixtures/hubzilla-create-image.json1
-rw-r--r--test/fixtures/image_with_imagedescription_and_caption-abstract_and_stray_data_after.pngbin0 -> 820 bytes
-rwxr-xr-xtest/fixtures/image_with_stray_data_after.pngbin0 -> 104435 bytes
-rw-r--r--test/fixtures/mastodon-nodeinfo20.json1
-rw-r--r--test/fixtures/mastodon-well-known-nodeinfo.json1
-rw-r--r--test/fixtures/minds-invalid-mention-post.json1
-rw-r--r--test/fixtures/minds-pleroma-mentioned-post.json1
-rw-r--r--test/fixtures/owncast-note-with-attachment.json31
-rw-r--r--test/fixtures/png_with_transparency.pngbin0 -> 84250 bytes
-rw-r--r--test/fixtures/quote_post/fedibird_quote_mismatched.json54
-rw-r--r--test/fixtures/quote_post/fedibird_quote_post.json52
-rw-r--r--test/fixtures/quote_post/fedibird_quote_uri.json54
-rw-r--r--test/fixtures/quote_post/fep-e232-tag-example.json17
-rw-r--r--test/fixtures/quote_post/misskey_quote_post.json46
-rw-r--r--test/fixtures/rich_media/google.html12
-rw-r--r--test/fixtures/rich_media/oembed.html2
-rw-r--r--test/fixtures/rich_media/reddit.html392
-rw-r--r--test/fixtures/rich_media/yahoo.html12
-rw-r--r--test/fixtures/rsa_keys/key_1.pem27
-rw-r--r--test/fixtures/rsa_keys/key_2.pem27
-rw-r--r--test/fixtures/rsa_keys/key_3.pem27
-rw-r--r--test/fixtures/rsa_keys/key_4.pem27
-rw-r--r--test/fixtures/rsa_keys/key_5.pem27
-rw-r--r--test/fixtures/tesla_mock/aimu@misskey.io.json64
-rw-r--r--test/fixtures/tesla_mock/framatube.org_host_meta2
-rw-r--r--test/fixtures/tesla_mock/gleasonator.com_host_meta4
-rw-r--r--test/fixtures/tesla_mock/helene@p.helene.moe.json50
-rw-r--r--test/fixtures/tesla_mock/https___lm.kazv.moe_users_mewmew.xml1
-rw-r--r--test/fixtures/tesla_mock/lm.kazv.moe_host_meta1
-rw-r--r--test/fixtures/tesla_mock/mametsuko@mk.absturztau.be.json65
-rw-r--r--test/fixtures/tesla_mock/mewmew@lm.kazv.moe.json1
-rw-r--r--test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json44
-rw-r--r--test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json1
-rw-r--r--test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg.json44
-rw-r--r--test/fixtures/tesla_mock/p.helene.moe-AM7S6vZQmL6pI9TgPY.json36
-rw-r--r--test/fixtures/tesla_mock/smithereen_non_anonymous_poll.json1
-rw-r--r--test/fixtures/tesla_mock/smithereen_user.json1
-rw-r--r--test/fixtures/tesla_mock/status.alpicola.com_host_meta2
-rw-r--r--test/fixtures/tesla_mock/webfinger_spoof.json28
-rw-r--r--test/fixtures/webfinger/graf-imposter-webfinger.json41
-rw-r--r--test/fixtures/webfinger/masto-host-meta.xml4
-rw-r--r--test/fixtures/webfinger/masto-user.json92
-rw-r--r--test/fixtures/webfinger/masto-webfinger.json23
-rw-r--r--test/fixtures/webfinger/pleroma-host-meta.xml1
-rw-r--r--test/fixtures/webfinger/pleroma-user.json58
-rw-r--r--test/fixtures/webfinger/pleroma-webfinger.json27
-rw-r--r--test/fixtures/wildebeest-nodeinfo21.json1
-rw-r--r--test/fixtures/wildebeest-well-known-nodeinfo.json1
-rw-r--r--test/fixtures/xml_billion_laughs.xml15
-rw-r--r--test/fixtures/xml_external_entities.xml3
-rw-r--r--test/mix/pleroma_test.exs2
-rw-r--r--test/mix/tasks/pleroma/config_test.exs13
-rw-r--r--test/mix/tasks/pleroma/digest_test.exs5
-rw-r--r--test/mix/tasks/pleroma/ecto/migrate_test.exs2
-rw-r--r--test/mix/tasks/pleroma/ecto/rollback_test.exs4
-rw-r--r--test/mix/tasks/pleroma/openapi_spec_test.exs62
-rw-r--r--test/mix/tasks/pleroma/robots_txt_test.exs2
-rw-r--r--test/mix/tasks/pleroma/user_test.exs5
-rw-r--r--test/pleroma/activity/ir/topics_test.exs105
-rw-r--r--test/pleroma/activity_test.exs75
-rw-r--r--test/pleroma/bbs/handler_test.exs89
-rw-r--r--test/pleroma/bookmark_folder_test.exs60
-rw-r--r--test/pleroma/bookmark_test.exs17
-rw-r--r--test/pleroma/config/deprecation_warnings_test.exs11
-rw-r--r--test/pleroma/config/loader_test.exs1
-rw-r--r--test/pleroma/config/release_runtime_provider_test.exs9
-rw-r--r--test/pleroma/config/transfer_task_test.exs83
-rw-r--r--test/pleroma/config_db_test.exs26
-rw-r--r--test/pleroma/conversation/participation_test.exs6
-rw-r--r--test/pleroma/conversation_test.exs5
-rw-r--r--test/pleroma/docs/translator/compiler_test.exs90
-rw-r--r--test/pleroma/ecto_type/activity_pub/object_validators/bare_uri_test.exs25
-rw-r--r--test/pleroma/emoji/loader_test.exs2
-rw-r--r--test/pleroma/emoji/pack_test.exs4
-rw-r--r--test/pleroma/emoji_test.exs26
-rw-r--r--test/pleroma/formatter_test.exs4
-rw-r--r--test/pleroma/healthcheck_test.exs8
-rw-r--r--test/pleroma/html_test.exs38
-rw-r--r--test/pleroma/http/adapter_helper/gun_test.exs4
-rw-r--r--test/pleroma/http/adapter_helper/hackney_test.exs2
-rw-r--r--test/pleroma/http/web_push_test.exs45
-rw-r--r--test/pleroma/instances/instance_test.exs68
-rw-r--r--test/pleroma/integration/mastodon_websocket_test.exs387
-rw-r--r--test/pleroma/maps_test.exs22
-rw-r--r--test/pleroma/mfa/totp_test.exs6
-rw-r--r--test/pleroma/notification_test.exs354
-rw-r--r--test/pleroma/object/fetcher_test.exs350
-rw-r--r--test/pleroma/object/updater_test.exs76
-rw-r--r--test/pleroma/object_test.exs41
-rw-r--r--test/pleroma/otp_version_test.exs2
-rw-r--r--test/pleroma/repo/migrations/autolinker_to_linkify_test.exs6
-rw-r--r--test/pleroma/repo/migrations/fix_malformed_formatter_config_test.exs4
-rw-r--r--test/pleroma/resilience_test.exs103
-rw-r--r--test/pleroma/reverse_proxy_test.exs2
-rw-r--r--test/pleroma/rule_test.exs57
-rw-r--r--test/pleroma/scheduled_activity_test.exs32
-rw-r--r--test/pleroma/search/database_search_test.exs (renamed from test/pleroma/activity/search_test.exs)24
-rw-r--r--test/pleroma/search/healthcheck_test.exs49
-rw-r--r--test/pleroma/search/meilisearch_test.exs160
-rw-r--r--test/pleroma/signature_test.exs12
-rw-r--r--test/pleroma/upload/filter/analyze_metadata_test.exs14
-rw-r--r--test/pleroma/upload/filter/exiftool/read_description_test.exs29
-rw-r--r--test/pleroma/upload/filter/exiftool/strip_location_test.exs21
-rw-r--r--test/pleroma/upload/filter/only_media_test.exs32
-rw-r--r--test/pleroma/upload_test.exs41
-rw-r--r--test/pleroma/uploaders/s3_test.exs89
-rw-r--r--test/pleroma/user/backup_async_test.exs51
-rw-r--r--test/pleroma/user/backup_test.exs166
-rw-r--r--test/pleroma/user/import_test.exs1
-rw-r--r--test/pleroma/user/query_test.exs61
-rw-r--r--test/pleroma/user_relationship_test.exs10
-rw-r--r--test/pleroma/user_search_test.exs9
-rw-r--r--test/pleroma/user_test.exs456
-rw-r--r--test/pleroma/web/activity_pub/activity_pub_controller_test.exs81
-rw-r--r--test/pleroma/web/activity_pub/activity_pub_test.exs112
-rw-r--r--test/pleroma/web/activity_pub/builder_test.exs29
-rw-r--r--test/pleroma/web/activity_pub/mrf/anti_link_spam_policy_test.exs33
-rw-r--r--test/pleroma/web/activity_pub/mrf/emoji_policy_test.exs425
-rw-r--r--test/pleroma/web/activity_pub/mrf/ensure_re_prepended_test.exs53
-rw-r--r--test/pleroma/web/activity_pub/mrf/follow_bot_policy_test.exs2
-rw-r--r--test/pleroma/web/activity_pub/mrf/force_mention_test.exs73
-rw-r--r--test/pleroma/web/activity_pub/mrf/force_mentions_in_content_test.exs146
-rw-r--r--test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs70
-rw-r--r--test/pleroma/web/activity_pub/mrf/inline_quote_policy_test.exs112
-rw-r--r--test/pleroma/web/activity_pub/mrf/keyword_policy_test.exs131
-rw-r--r--test/pleroma/web/activity_pub/mrf/media_proxy_warming_policy_test.exs53
-rw-r--r--test/pleroma/web/activity_pub/mrf/no_empty_policy_test.exs23
-rw-r--r--test/pleroma/web/activity_pub/mrf/no_placeholder_text_policy_test.exs41
-rw-r--r--test/pleroma/web/activity_pub/mrf/normalize_markup_test.exs59
-rw-r--r--test/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy_test.exs73
-rw-r--r--test/pleroma/web/activity_pub/mrf/simple_policy_test.exs22
-rw-r--r--test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs53
-rw-r--r--test/pleroma/web/activity_pub/mrf/tag_policy_test.exs36
-rw-r--r--test/pleroma/web/activity_pub/mrf/utils_test.exs19
-rw-r--r--test/pleroma/web/activity_pub/mrf_test.exs15
-rw-r--r--test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs133
-rw-r--r--test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs20
-rw-r--r--test/pleroma/web/activity_pub/object_validators/chat_validation_test.exs26
-rw-r--r--test/pleroma/web/activity_pub/object_validators/create_generic_validator_test.exs9
-rw-r--r--test/pleroma/web/activity_pub/object_validators/delete_validation_test.exs17
-rw-r--r--test/pleroma/web/activity_pub/object_validators/emoji_react_handling_test.exs60
-rw-r--r--test/pleroma/web/activity_pub/object_validators/update_handling_test.exs126
-rw-r--r--test/pleroma/web/activity_pub/publisher_test.exs105
-rw-r--r--test/pleroma/web/activity_pub/side_effects_test.exs415
-rw-r--r--test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs82
-rw-r--r--test/pleroma/web/activity_pub/transmogrifier/image_handling_test.exs50
-rw-r--r--test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs55
-rw-r--r--test/pleroma/web/activity_pub/transmogrifier/question_handling_test.exs3
-rw-r--r--test/pleroma/web/activity_pub/transmogrifier/undo_handling_test.exs2
-rw-r--r--test/pleroma/web/activity_pub/transmogrifier_test.exs178
-rw-r--r--test/pleroma/web/activity_pub/utils_test.exs115
-rw-r--r--test/pleroma/web/activity_pub/views/object_view_test.exs14
-rw-r--r--test/pleroma/web/activity_pub/views/user_view_test.exs23
-rw-r--r--test/pleroma/web/activity_pub/visibility_test.exs50
-rw-r--r--test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs275
-rw-r--r--test/pleroma/web/admin_api/controllers/announcement_controller_test.exs96
-rw-r--r--test/pleroma/web/admin_api/controllers/chat_controller_test.exs44
-rw-r--r--test/pleroma/web/admin_api/controllers/config_controller_test.exs73
-rw-r--r--test/pleroma/web/admin_api/controllers/frontend_controller_test.exs1
-rw-r--r--test/pleroma/web/admin_api/controllers/instance_controller_test.exs14
-rw-r--r--test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs2
-rw-r--r--test/pleroma/web/admin_api/controllers/invite_controller_test.exs72
-rw-r--r--test/pleroma/web/admin_api/controllers/media_proxy_cache_controller_test.exs10
-rw-r--r--test/pleroma/web/admin_api/controllers/o_auth_app_controller_test.exs2
-rw-r--r--test/pleroma/web/admin_api/controllers/report_controller_test.exs175
-rw-r--r--test/pleroma/web/admin_api/controllers/rule_controller_test.exs82
-rw-r--r--test/pleroma/web/admin_api/controllers/status_controller_test.exs44
-rw-r--r--test/pleroma/web/admin_api/controllers/user_controller_test.exs302
-rw-r--r--test/pleroma/web/admin_api/views/report_view_test.exs25
-rw-r--r--test/pleroma/web/api_spec/scopes/compiler_test.exs56
-rw-r--r--test/pleroma/web/common_api/activity_draft_test.exs33
-rw-r--r--test/pleroma/web/common_api/utils_test.exs84
-rw-r--r--test/pleroma/web/common_api_test.exs451
-rw-r--r--test/pleroma/web/endpoint/metrics_exporter_test.exs69
-rw-r--r--test/pleroma/web/fallback_test.exs41
-rw-r--r--test/pleroma/web/federator_test.exs50
-rw-r--r--test/pleroma/web/feed/tag_controller_test.exs5
-rw-r--r--test/pleroma/web/feed/user_controller_test.exs48
-rw-r--r--test/pleroma/web/mastodon_api/controllers/account_controller_test.exs270
-rw-r--r--test/pleroma/web/mastodon_api/controllers/directory_controller_test.exs2
-rw-r--r--test/pleroma/web/mastodon_api/controllers/filter_controller_test.exs57
-rw-r--r--test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs63
-rw-r--r--test/pleroma/web/mastodon_api/controllers/media_controller_test.exs28
-rw-r--r--test/pleroma/web/mastodon_api/controllers/notification_controller_test.exs78
-rw-r--r--test/pleroma/web/mastodon_api/controllers/report_controller_test.exs76
-rw-r--r--test/pleroma/web/mastodon_api/controllers/scheduled_activity_controller_test.exs18
-rw-r--r--test/pleroma/web/mastodon_api/controllers/search_controller_test.exs81
-rw-r--r--test/pleroma/web/mastodon_api/controllers/status_controller_test.exs837
-rw-r--r--test/pleroma/web/mastodon_api/controllers/subscription_controller_test.exs15
-rw-r--r--test/pleroma/web/mastodon_api/controllers/timeline_controller_test.exs55
-rw-r--r--test/pleroma/web/mastodon_api/mastodon_api_test.exs5
-rw-r--r--test/pleroma/web/mastodon_api/update_credentials_test.exs219
-rw-r--r--test/pleroma/web/mastodon_api/views/account_view_test.exs182
-rw-r--r--test/pleroma/web/mastodon_api/views/notification_view_test.exs106
-rw-r--r--test/pleroma/web/mastodon_api/views/poll_view_test.exs10
-rw-r--r--test/pleroma/web/mastodon_api/views/scheduled_activity_view_test.exs9
-rw-r--r--test/pleroma/web/mastodon_api/views/status_view_test.exs328
-rw-r--r--test/pleroma/web/media_proxy/invalidation/script_test.exs13
-rw-r--r--test/pleroma/web/media_proxy/media_proxy_controller_test.exs42
-rw-r--r--test/pleroma/web/media_proxy_test.exs17
-rw-r--r--test/pleroma/web/metadata/providers/open_graph_test.exs10
-rw-r--r--test/pleroma/web/metadata/providers/rel_me_test.exs17
-rw-r--r--test/pleroma/web/metadata/providers/twitter_card_test.exs82
-rw-r--r--test/pleroma/web/metadata/utils_test.exs54
-rw-r--r--test/pleroma/web/node_info_test.exs15
-rw-r--r--test/pleroma/web/o_auth/mfa_controller_test.exs2
-rw-r--r--test/pleroma/web/o_auth/o_auth_controller_test.exs6
-rw-r--r--test/pleroma/web/o_auth/token/utils_test.exs4
-rw-r--r--test/pleroma/web/o_status/o_status_controller_test.exs52
-rw-r--r--test/pleroma/web/pleroma_api/controllers/backup_controller_test.exs27
-rw-r--r--test/pleroma/web/pleroma_api/controllers/bookmark_folder_controller_test.exs161
-rw-r--r--test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs55
-rw-r--r--test/pleroma/web/pleroma_api/controllers/emoji_file_controller_test.exs30
-rw-r--r--test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs80
-rw-r--r--test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs196
-rw-r--r--test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs4
-rw-r--r--test/pleroma/web/pleroma_api/controllers/mascot_controller_test.exs9
-rw-r--r--test/pleroma/web/pleroma_api/controllers/notification_controller_test.exs27
-rw-r--r--test/pleroma/web/pleroma_api/controllers/scrobble_controller_test.exs12
-rw-r--r--test/pleroma/web/pleroma_api/controllers/settings_controller_test.exs126
-rw-r--r--test/pleroma/web/pleroma_api/controllers/status_controller_test.exs54
-rw-r--r--test/pleroma/web/pleroma_api/views/backup_view_test.exs50
-rw-r--r--test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs28
-rw-r--r--test/pleroma/web/plugs/authentication_plug_test.exs30
-rw-r--r--test/pleroma/web/plugs/ensure_privileged_plug_test.exs96
-rw-r--r--test/pleroma/web/plugs/ensure_staff_privileged_plug_test.exs60
-rw-r--r--test/pleroma/web/plugs/frontend_static_plug_test.exs10
-rw-r--r--test/pleroma/web/plugs/http_security_plug_test.exs4
-rw-r--r--test/pleroma/web/plugs/rate_limiter_test.exs44
-rw-r--r--test/pleroma/web/plugs/uploaded_media_plug_test.exs19
-rw-r--r--test/pleroma/web/push/impl_test.exs20
-rw-r--r--test/pleroma/web/rich_media/card_test.exs71
-rw-r--r--test/pleroma/web/rich_media/helpers_test.exs84
-rw-r--r--test/pleroma/web/rich_media/parser/ttl/aws_signed_url_test.exs56
-rw-r--r--test/pleroma/web/rich_media/parser/ttl/opengraph_test.exs41
-rw-r--r--test/pleroma/web/rich_media/parser_test.exs132
-rw-r--r--test/pleroma/web/streamer_test.exs317
-rw-r--r--test/pleroma/web/twitter_api/remote_follow_controller_test.exs88
-rw-r--r--test/pleroma/web/twitter_api/util_controller_test.exs465
-rw-r--r--test/pleroma/web/web_finger/web_finger_controller_test.exs28
-rw-r--r--test/pleroma/web/web_finger_test.exs85
-rw-r--r--test/pleroma/web/xml_test.exs15
-rw-r--r--test/pleroma/workers/cron/digest_emails_worker_test.exs5
-rw-r--r--test/pleroma/workers/cron/new_users_digest_worker_test.exs5
-rw-r--r--test/pleroma/workers/publisher_worker_test.exs40
-rw-r--r--test/pleroma/workers/purge_expired_token_test.exs2
-rw-r--r--test/pleroma/workers/receiver_worker_test.exs52
-rw-r--r--test/pleroma/workers/remote_fetcher_worker_test.exs69
-rw-r--r--test/support/cachex_proxy.ex6
-rw-r--r--test/support/cluster.ex15
-rw-r--r--test/support/data_case.ex1
-rw-r--r--test/support/factory.ex27
-rw-r--r--test/support/helpers.ex35
-rw-r--r--test/support/http_request_mock.ex359
-rw-r--r--test/support/mocks.ex6
-rw-r--r--test/support/null_cache.ex6
-rw-r--r--test/support/websocket_client.ex32
-rw-r--r--test/test_helper.exs26
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
new file mode 100644
index 000000000..7ce8640fa
--- /dev/null
+++ b/test/fixtures/image_with_imagedescription_and_caption-abstract_and_stray_data_after.png
Binary files differ
diff --git a/test/fixtures/image_with_stray_data_after.png b/test/fixtures/image_with_stray_data_after.png
new file mode 100755
index 000000000..a280e4377
--- /dev/null
+++ b/test/fixtures/image_with_stray_data_after.png
Binary files differ
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
new file mode 100644
index 000000000..7963149db
--- /dev/null
+++ b/test/fixtures/png_with_transparency.png
Binary files differ
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">&nbsp;|&nbsp;</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 &raquo;</a></div></div><div id="header-bottom-left"><a href="/" id="header-img" class="default-header" title="">reddit.com</a>&nbsp;<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?&#32;<a href="https://www.reddit.com/login" class="login-required login-link" >Log in</a>&#32;or&#32;<a href="https://www.reddit.com/login" class="login-required" >sign up</a>&#32;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 &quot;subreddit&quot;</dd><dt>author:<i>username</i></dt><dd>find submissions by &quot;username&quot;</dd><dt>site:<i>example.com</i></dt><dd>find submissions from &quot;example.com&quot;</dd><dt>url:<i>text</i></dt><dd>search for &quot;text&quot; in url</dd><dt>selftext:<i>text</i></dt><dd>search for &quot;text&quot; 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.&#32;<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 &#32;</span><time datetime="2023-09-20T07:34:36+00:00">20 Sep 2023</time></div><div class="score"><span class="number">241</span>&#32;<span class="word">points</span>&#32;(92% upvoted)</div><div class="shortlink">shortlink: &#32;<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>&#32;<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>&#32;<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 &#39;old&#39; Reddit is no longer kept up to date. Please use &#39;new&#39; Reddit. </p>
+
+<ol>
+<li><p><strong>No Low Effort / Poor Quality Posts</strong>
+&quot;This security forum is oriented towards private white hat security professionals.&quot; If a post has very basic information, it is not appropriate for this sub. For example, &quot;why passwords are important&quot; 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&#39;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&#39;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&#32;<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&#32;<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">&lt;</span><span class="read-next-button next">&gt;</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>&#32; &middot;&#32;<span class="comments">45 comments</span>&#32;</div><div class="read-next-title">What are some &quot;must-have&quot; 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>&#32; &middot;&#32;<span class="comments">27 comments</span>&#32;</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>&#32; &middot;&#32;<span class="comments">17 comments</span>&#32;</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>&#32; &middot;&#32;<span class="comments">57 comments</span>&#32;</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>&#32; &middot;&#32;<span class="comments">8 comments</span>&#32;</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>&#32; &middot;&#32;<span class="comments">296 comments</span>&#32;</div><div class="read-next-title">What is a security feature that is really &quot;security theater&quot;?</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>&#32; &middot;&#32;<span class="comments">17 comments</span>&#32;</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>&#32; &middot;&#32;<span class="comments">9 comments</span>&#32;</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>&#32; &middot;&#32;<span class="comments">1 comment</span>&#32;</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">&times;</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&#32; 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>&#32;<span class="domain">(<a href="/r/cybersecurity/">self.cybersecurity</a>)</span></p><p class="tagline ">submitted&#32;<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>&#32;by&#32;<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&#39;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:&#32;</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&amp;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;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 &#32; 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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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, &amp; Compliance" style="background-color: #373c3f; border-color: #373c3f;"><span>Governance, Risk, &amp; Compliance</span></span><span class="userattrs"></span>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;m sure Jerry Bell is cool and all but I&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#39;shitty admin move&#39; 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 &#39;private&#39; 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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;s also helpful to follow some hashtags (which the Mastodon android app can&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;t nearly as populated as it&#39;s zealots would lead you to believe, unfortunately. Great concept, but just doesn&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;t see how it&#39;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&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;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 &#32; 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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;t exist.</p>
+
+<p>I don&#39;t care about who posted most recently, I want to know what&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;t think algorithms are inherently bad as long as they can&#39;t be manipulated.</p>
+
+<p>That being said, Reddit mostly managed without algorithms thanks to voting &amp; community driven recommendations, but Mastadon doesn&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;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&#39;t see ads on almost any site with this method. At work I get ads all the time and it&#39;s annoying. Not sure how y&#39;all lived like this for so long. Ads haven&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;m hoping Bluesky gets more popular once it actually opens. I was able to join yesterday and there&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;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&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;m like a lot of people who have tried mastodon and nope out. </p>
+
+<p>I get the idea, but I still don&#39;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&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;m the same way. I was pretty pumped to have another platform, I&#39;m pretty over this website and I&#39;ve never been into twitter. It&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;t know why I do anymore, it&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;s an interesting one. I&#39;ll have a look as I&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;t the /sec/ general completely dead or at the very least only filled with &quot;How to become 1337 haxxor&quot;?</p>
+
+<p>It&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&amp;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&amp;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#32;are&#32;automatically&#32;archived&#32;after&#32;6&#32;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&nbsp;<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>&nbsp;or&nbsp;<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&#32;are&#32;automatically&#32;archived&#32;after&#32;6&#32;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&nbsp;<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>&nbsp;or&nbsp;<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 &amp; 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">&lt;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&#32;<a href="https://www.reddit.com/help/useragreement" >User Agreement</a>&#32;and&#32;<a href="https://www.reddit.com/help/privacypolicy" >Privacy Policy</a>. &copy; 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>&nbsp|<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&#32;<a href="https://www.reddit.com/help/useragreement/" >Terms</a>&#32;and that you have read our&#32;<a href="https://www.reddit.com/help/privacypolicy/" >Privacy Policy</a>&#32;and&#32;<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>&nbsp;<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>&#32;<span class="details hover">(*) incomplete &#32;<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">&pi;</span>&nbsp;<span class="content">Rendered by PID 29 on&#32; reddit-service-r2-slowlane-65c5c76ff5-v258h &#32;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=&quot;https://www.w3.org/ns/activitystreams&quot;" /><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 &quot;rel&quot; attribute: example.com
this is a link with not allowed &quot;rel&quot; attribute: example.com
this is an image:
+ this is an inline emoji:
alert(&#39;hacked&#39;)
"""
@@ -67,6 +73,7 @@ defmodule Pleroma.HTMLTest do
this is a link with allowed &quot;rel&quot; attribute: <a href="http://example.com/" rel="tag">example.com</a>
this is a link with not allowed &quot;rel&quot; 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(&#39;hacked&#39;)
"""
@@ -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 &quot;rel&quot; attribute: <a href="http://example.com/" rel="tag">example.com</a>
this is a link with not allowed &quot;rel&quot; 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(&#39;hacked&#39;)
"""
@@ -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&#39;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&#39;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 &quot;rel&quot; attribute: <a href="http://example.com/" rel="tag">example.com</a>
- this is a link with not allowed &quot;rel&quot; attribute: <a href="http://example.com/">example.com</a>
- this is an image: <img src="http://example.com/image.jpg"/><br/>
- alert(&#39;hacked&#39;)
- """
+ @expected """
+ <b>this is in bold</b>
+ <p>this is a paragraph</p>
+ this is a linebreak<br/>
+ this is a link with allowed &quot;rel&quot; attribute: <a href="http://example.com/" rel="tag">example.com</a>
+ this is a link with not allowed &quot;rel&quot; attribute: <a href="http://example.com/">example.com</a>
+ this is an image: <img src="http://example.com/image.jpg"/><br/>
+ alert(&#39;hacked&#39;)
+ """
+ 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&#39;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&#39;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&#39;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(&#39;xss&#39;)"
- 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 &amp; Thi...', 'This &amp; 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 &amp; 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 &amp; Thi...', 'This &amp; 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 &amp; 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